diff --git a/.github/images/observer.gif b/.github/images/observer.gif deleted file mode 100644 index 9ccadfb5..00000000 Binary files a/.github/images/observer.gif and /dev/null differ diff --git a/ChangeLog.md b/ChangeLog.md index 0c4e4773..0bd52aa7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,69 @@ All notable changes to this project will be documented in this file. This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +#### [v2.2.0](https://github.com/ergo-services/ergo/releases/tag/v1.999.220) 2022-10-18 [tag version v1.999.220] #### + +* Introduced `gen.Web` behavior. It implements **Web API Gateway pattern** is also sometimes known as the "Backend For Frontend" (BFF). See example [examples/genweb](examples/genweb) +* Introduced `gen.TCP` behavior - **socket acceptor pool for TCP protocols**. It provides everything you need to accept TCP connections and process packets with a small code base and low latency. Here is simple example [examples/gentcp](examples/gentcp) +* Introduced `gen.UDP` - the same as `gen.TCP`, but for UDP protocols. Example is here [examples/genudp](examples/genudp) +* Introduced **Events**. This is a simple pub/sub feature within a node - any `gen.Process` can become a producer by registering a new event `gen.Event` using method `gen.Process.RegisterEvent`, while the others can subscribe to these events using `gen.Process.MonitorEvent`. Subscriber process will also receive `gen.MessageEventDown` if a producer process went down (terminated). This feature behaves in a monitor manner but only works within a node. You may also want to subscribe to a system event - `node.EventNetwork` to receive event notification on connect/disconnect any peers. +* Introduced **Cloud Client** - allows connecting to the cloud platform [https://ergo.sevices](https://ergo.services). You may want to register your email there, and we will inform you about the platform launch day +* Introduced **type registration** for the ETF encoding/decoding. This feature allows you to get rid of manually decoding with `etf.TermIntoStruct` for the receiving messages. Register your type using `etf.RegisterType(...)`, and you will be receiving messages in a native type +* Predefined set of errors has moved to the `lib` package +* Updated `gen.ServerBehavior.HandleDirect` method (got extra argument `etf.Ref` to distinguish the requests). This change allows you to handle these requests asynchronously using method `gen.ServerProcess.Reply(...)` +* Updated `node.Options`. Now it has field `Listeners` (type `node.Listener`). It allows you to start any number of listeners with custom options - `Port`, `TLS` settings, or custom `Handshake`/`Proto` interfaces +* Fixed build on 32-bit arch +* Fixed freezing on ARM arch #102 +* Fixed problem with encoding negative int8 +* Fixed #103 (there was an issue on interop with Elixir's GenStage) +* Fixed node stuck on start if it uses the name which is already taken in EPMD +* Fixed incorrect `gen.ProcessOptions.Context` handling + + +#### [v2.1.0](https://github.com/ergo-services/ergo/releases/tag/v1.999.210) 2022-04-19 [tag version v1.999.210] #### + +* Introduced **compression feature** support. Here are new methods and options to manage this feature: + - `gen.Process`: + - `SetCompression(enable bool)`, `Compression() bool` + - `SetCompressionLevel(level int) bool`, `CompressionLevel() int` + - `SetCompressionThreshold(threshold int) bool`, `CompressionThreshold() int` messages smaller than the threshold will be sent with no compression. The default compression threshold is 1024 bytes. + - `node.Options`: + - `Compression` these settings are used as defaults for the spawning processes + - this feature will be ignored if the receiver is running on either the Erlang or Elixir node +* Introduced **proxy feature** support **with end-to-end encryption**. + - `node.Node` new methods: + - `AddProxyRoute(...)`, `RemoveProxyRoute(...)` + - `ProxyRoute(...)`, `ProxyRoutes()` + - `NodesIndirect()` returns list of connected nodes via proxy connection + - `node.Options`: + - `Proxy` for configuring proxy settings + - includes support (over the proxy connection): compression, fragmentation, link/monitor process, monitor node + - example [examples/proxy](examples/proxy). + - this feature is not available for the Erlang/Elixir nodes +* Introduced **behavior `gen.Raft`**. It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/genraft](examples/genraft). +* Introduced **interfaces to customize network layer** + - `Resolver` to replace EPMD routines with your solution (e.g., ZooKeeper or any other service registrar) + - `Handshake` allows customizing authorization/authentication process + - `Proto` provides the way to implement proprietary protocols (e.g., IoT area) +* Other new features: + - `gen.Process` new methods: + - `NodeUptime()`, `NodeName()`, `NodeStop()` + - `gen.ServerProcess` new method: + - `MessageCounter()` shows how many messages have been handled by the `gen.Server` callbacks + - `gen.ProcessOptions` new option: + - `ProcessFallback` allows forward messages to the fallback process if the process mailbox is full. Forwarded messages are wrapped into `gen.MessageFallback` struct. Related to issue #96. + - `gen.SupervisorChildSpec` and `gen.ApplicationChildSpec` got option `gen.ProcessOptions` to customize options for the spawning child processes. +* Improved sending messages by etf.Pid or etf.Alias: methods `gen.Process.Send`, `gen.ServerProcess.Cast`, `gen.ServerProcess.Call` now return `node.ErrProcessIncarnation` if a message is sending to the remote process of the previous incarnation (remote node has been restarted). Making monitor on a remote process of the previous incarnation triggers sending `gen.MessageDown` with reason `incarnation`. +* Introduced type `gen.EnvKey` for the environment variables +* All spawned processes now have the `node.EnvKeyNode` variable to get access to the `node.Node` value. +* **Improved performance** of local messaging (**up to 8 times** for some cases) +* **Important** `node.Options` has changed. Make sure to adjust your code. +* Fixed issue #89 (incorrect handling of Call requests) +* Fixed issues #87, #88 and #93 (closing network socket) +* Fixed issue #96 (silently drops message if process mailbox is full) +* Updated minimal requirement of Golang version to 1.17 (go.mod) +* We still keep the rule **Zero Dependencies** + #### [v2.0.0](https://github.com/ergo-services/ergo/releases/tag/v1.999.200) 2021-10-12 [tag version v1.999.200] #### * Added support of Erlang/OTP 24 (including [Alias](https://blog.erlang.org/My-OTP-24-Highlights/#eep-53-process-aliases) feature and [Remote Spawn](https://blog.erlang.org/OTP-23-Highlights/#distributed-spawn-and-the-new-erpc-module) introduced in Erlang/OTP 23) diff --git a/README.md b/README.md index 3c7410a3..6f4b9fe5 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ [![GoDoc](https://pkg.go.dev/badge/ergo-services/ergo)](https://pkg.go.dev/github.com/ergo-services/ergo) -[![Build Status](https://img.shields.io/github/workflow/status/ergo-services/ergo/TestLinuxWindowsMacOS)](https://github.com/ergo-services/ergo/actions/) +[![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![Telegram Community](https://img.shields.io/badge/Telegram-Community-blue?style=flat&logo=telegram)](https://t.me/ergo_services) [![Discord Community](https://img.shields.io/badge/Discord-Community-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/sdscxKGV62) [![Twitter](https://img.shields.io/badge/Twitter-ergo__services-1DA1F2?style=flat&logo=twitter&logoColor=white)](https://twitter.com/ergo_services) -[![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) Technologies and design patterns of Erlang/OTP have been proven over the years. Now in Golang. Up to x5 times faster than original Erlang/OTP in terms of network messaging. @@ -37,7 +36,7 @@ The goal of this project is to leverage Erlang/OTP experience with Golang perfor * Transient * `gen.Stage` behavior support (originated from Elixir's [GenStage](https://hexdocs.pm/gen_stage/GenStage.html)). This is abstraction built on top of `gen.Server` to provide a simple way to create a distributed Producer/Consumer architecture, while automatically managing the concept of backpressure. This implementation is fully compatible with Elixir's GenStage. Example is here [examples/genstage](examples/genstage) or just run `go run ./examples/genstage` to see it in action * `gen.Saga` behavior support. It implements Saga design pattern - a sequence of transactions that updates each service state and publishes the result (or cancels the transaction or triggers the next transaction step). `gen.Saga` also provides a feature of interim results (can be used as transaction progress or as a part of pipeline processing), time deadline (to limit transaction lifespan), two-phase commit (to make distributed transaction atomic). Here is example [examples/gensaga](examples/gensaga). -* `gen.Raft` behavior support. It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/raft](examples/raft). +* `gen.Raft` behavior support. It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/genraft](examples/genraft). * Connect to (accept connection from) any Erlang/Elixir node within a cluster * Making sync request `ServerProcess.Call`, async - `ServerProcess.Cast` or `Process.Send` in fashion of `gen_server:call`, `gen_server:cast`, `erlang:send` accordingly * Monitor processes/nodes, local/remote @@ -64,50 +63,23 @@ Golang introduced [v2 rule](https://go.dev/blog/v2-go-modules) a while ago to so Here are the changes of latest release. For more details see the [ChangeLog](ChangeLog.md) -#### [v2.1.0](https://github.com/ergo-services/ergo/releases/tag/v1.999.210) 2022-04-19 [tag version v1.999.210] #### - -* Introduced **compression feature** support. Here are new methods and options to manage this feature: - - `gen.Process`: - - `SetCompression(enable bool)`, `Compression() bool` - - `SetCompressionLevel(level int) bool`, `CompressionLevel() int` - - `SetCompressionThreshold(threshold int) bool`, `CompressionThreshold() int` messages smaller than the threshold will be sent with no compression. The default compression threshold is 1024 bytes. - - `node.Options`: - - `Compression` these settings are used as defaults for the spawning processes - - this feature will be ignored if the receiver is running on either the Erlang or Elixir node -* Introduced **proxy feature** support **with end-to-end encryption**. - - `node.Node` new methods: - - `AddProxyRoute(...)`, `RemoveProxyRoute(...)` - - `ProxyRoute(...)`, `ProxyRoutes()` - - `NodesIndirect()` returns list of connected nodes via proxy connection - - `node.Options`: - - `Proxy` for configuring proxy settings - - includes support (over the proxy connection): compression, fragmentation, link/monitor process, monitor node - - example [examples/proxy](examples/proxy). - - this feature is not available for the Erlang/Elixir nodes -* Introduced **behavior `gen.Raft`**. It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/raft](examples/raft). -* Introduced **interfaces to customize network layer** - - `Resolver` to replace EPMD routines with your solution (e.g., ZooKeeper or any other service registrar) - - `Handshake` allows customizing authorization/authentication process - - `Proto` provides the way to implement proprietary protocols (e.g., IoT area) -* Other new features: - - `gen.Process` new methods: - - `NodeUptime()`, `NodeName()`, `NodeStop()` - - `gen.ServerProcess` new method: - - `MessageCounter()` shows how many messages have been handled by the `gen.Server` callbacks - - `gen.ProcessOptions` new option: - - `ProcessFallback` allows forward messages to the fallback process if the process mailbox is full. Forwarded messages are wrapped into `gen.MessageFallback` struct. Related to issue #96. - - `gen.SupervisorChildSpec` and `gen.ApplicationChildSpec` got option `gen.ProcessOptions` to customize options for the spawning child processes. -* Improved sending messages by etf.Pid or etf.Alias: methods `gen.Process.Send`, `gen.ServerProcess.Cast`, `gen.ServerProcess.Call` now return `node.ErrProcessIncarnation` if a message is sending to the remote process of the previous incarnation (remote node has been restarted). Making monitor on a remote process of the previous incarnation triggers sending `gen.MessageDown` with reason `incarnation`. -* Introduced type `gen.EnvKey` for the environment variables -* All spawned processes now have the `node.EnvKeyNode` variable to get access to the `node.Node` value. -* **Improved performance** of local messaging (**up to 8 times** for some cases) -* **Important** `node.Options` has changed. Make sure to adjust your code. -* Fixed issue #89 (incorrect handling of Call requests) -* Fixed issues #87, #88 and #93 (closing network socket) -* Fixed issue #96 (silently drops message if process mailbox is full) -* Updated minimal requirement of Golang version to 1.17 (go.mod) -* We still keep the rule **Zero Dependencies** - +#### [v2.2.0](https://github.com/ergo-services/ergo/releases/tag/v1.999.220) 2022-10-18 [tag version v1.999.220] #### + +* Introduced `gen.Web` behavior. It implements **Web API Gateway pattern** is also sometimes known as the "Backend For Frontend" (BFF). See example [examples/genweb](examples/genweb) +* Introduced `gen.TCP` behavior - **socket acceptor pool for TCP protocols**. It provides everything you need to accept TCP connections and process packets with a small code base and low latency. Here is simple example [examples/gentcp](examples/gentcp) +* Introduced `gen.UDP` - the same as `gen.TCP`, but for UDP protocols. Example is here [examples/genudp](examples/genudp) +* Introduced **Events**. This is a simple pub/sub feature within a node - any `gen.Process` can become a producer by registering a new event `gen.Event` using method `gen.Process.RegisterEvent`, while the others can subscribe to these events using `gen.Process.MonitorEvent`. Subscriber process will also receive `gen.MessageEventDown` if a producer process went down (terminated). This feature behaves in a monitor manner but only works within a node. You may also want to subscribe to a system event - `node.EventNetwork` to receive event notification on connect/disconnect any peers. Here is simple example of this feature [examples/events](examples/events) +* Introduced **Cloud Client** - allows connecting to the cloud platform [https://ergo.sevices](https://ergo.services). You may want to register your email there, and we will inform you about the platform launch day +* Introduced **type registration** for the ETF encoding/decoding. This feature allows you to get rid of manually decoding with `etf.TermIntoStruct` for the receiving messages. Register your type using `etf.RegisterType(...)`, and you will be receiving messages in a native type +* Predefined set of errors has moved to the `lib` package +* Updated `gen.ServerBehavior.HandleDirect` method (got extra argument `etf.Ref` to distinguish the requests). This change allows you to handle these requests asynchronously using method `gen.ServerProcess.Reply(...)` +* Updated `node.Options`. Now it has field `Listeners` (type `node.Listener`). It allows you to start any number of listeners with custom options - `Port`, `TLS` settings, or custom `Handshake`/`Proto` interfaces +* Fixed build on 32-bit arch +* Fixed freezing on ARM arch #102 +* Fixed problem with encoding negative int8 +* Fixed #103 (there was an issue on interop with Elixir's GenStage) +* Fixed node stuck on start if it uses the name which is already taken in EPMD +* Fixed incorrect `gen.ProcessOptions.Context` handling ### Benchmarks ### @@ -183,17 +155,9 @@ sources of these benchmarks are [here](https://github.com/halturin/ergobenchmark The one thing that makes embedded EPMD different is the behavior of handling connection hangs - if ergo' node is running as an EPMD client and lost connection, it tries either to run its own embedded EPMD service or to restore the lost connection. -### Observer ### - -It's a standard Erlang tool. Observer is a graphical tool for observing the characteristics of Erlang systems. The tool Observer displays system information, application supervisor trees, process information. - -Here you can see this feature in action using one of the [examples](examples/): - -![observer demo](.github/images/observer.gif) - ### Examples ### -Code below is a simple implementation of gen.Server pattern [examples/simple](examples/simple) +Code below is a simple implementation of gen.Server pattern [examples/genserver](examples/genserver) ```golang package main @@ -202,13 +166,10 @@ import ( "fmt" "time" - "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" - "github.com/ergo-services/ergo/node" ) -// simple implementation of Server type simple struct { gen.Server } @@ -219,27 +180,12 @@ func (s *simple) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.Se if value > 104 { return gen.ServerStatusStop } - // sending message with delay - process.SendAfter(process.Self(), value+1, time.Duration(1*time.Second)) + // sending message with delay 1 second + fmt.Println("increase this value by 1 and send it to itself again") + process.SendAfter(process.Self(), value+1, time.Second) return gen.ServerStatusOK } -func main() { - // create a new node - node, _ := ergo.StartNode("node@localhost", "cookies", node.Options{}) - - // spawn a new process of gen.Server - process, _ := node.Spawn("gs1", gen.ProcessOptions{}, &simple{}) - - // send a message to itself - process.Send(process.Self(), 100) - - // wait for the process termination. - process.Wait() - fmt.Println("exited") - node.Stop() -} - ``` here is output of this code @@ -262,9 +208,14 @@ See `examples/` for more details * [gen.Server](examples/genserver) * [gen.Stage](examples/genstage) * [gen.Saga](examples/gensaga) -* [gen.Demo](examples/gendemo) -* [Node with TLS](examples/nodetls) -* [Node with HTTP server](examples/http) +* [gen.Raft](examples/genraft) +* [gen.Custom](examples/gencustom) +* [gen.Web](examples/genweb) +* [gen.TCP](examples/gentcp) +* [gen.UDP](examples/genudp) +* [events](examples/events) +* [erlang](examples/erlang) +* [proxy](examples/proxy) ### Elixir Phoenix Users ### diff --git a/apps/cloud/app.go b/apps/cloud/app.go new file mode 100644 index 00000000..9f103fa4 --- /dev/null +++ b/apps/cloud/app.go @@ -0,0 +1,42 @@ +package cloud + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +type CloudApp struct { + gen.Application + options node.Cloud +} + +func CreateApp(options node.Cloud) gen.ApplicationBehavior { + if options.Flags.Enable == false { + options.Flags = node.DefaultCloudFlags() + } + return &CloudApp{ + options: options, + } +} + +func (ca *CloudApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { + lib.Log("CLOUD_CLIENT: Application load") + return gen.ApplicationSpec{ + Name: "cloud_app", + Description: "Ergo Cloud Support Application", + Version: "v.1.0", + Children: []gen.ApplicationChildSpec{ + gen.ApplicationChildSpec{ + Child: &cloudAppSup{}, + Name: "cloud_app_sup", + Args: []etf.Term{ca.options}, + }, + }, + }, nil +} + +func (ca *CloudApp) Start(p gen.Process, args ...etf.Term) { + lib.Log("[%s] CLOUD_CLIENT: Application started", p.NodeName()) +} diff --git a/apps/cloud/client.go b/apps/cloud/client.go new file mode 100644 index 00000000..38e08cdd --- /dev/null +++ b/apps/cloud/client.go @@ -0,0 +1,216 @@ +package cloud + +import ( + "crypto/tls" + "fmt" + "net" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +type CloudNode struct { + Node string + Port uint16 + SkipVerify bool +} + +type cloudClient struct { + gen.Server +} + +type cloudClientState struct { + options node.Cloud + handshake node.HandshakeInterface + monitor etf.Ref + node string +} + +type messageCloudClientConnect struct{} + +func (cc *cloudClient) Init(process *gen.ServerProcess, args ...etf.Term) error { + lib.Log("[%s] CLOUD_CLIENT: Init: %#v", process.NodeName(), args) + if len(args) == 0 { + return fmt.Errorf("no args to start cloud client") + } + + cloudOptions, ok := args[0].(node.Cloud) + if ok == false { + return fmt.Errorf("wrong args for the cloud client") + } + + handshake, err := createHandshake(cloudOptions) + if err != nil { + return fmt.Errorf("can not create HandshakeInterface for the cloud client: %s", err) + } + + process.State = &cloudClientState{ + options: cloudOptions, + handshake: handshake, + } + + if err := process.RegisterEvent(EventCloud, []gen.EventMessage{MessageEventCloud{}}); err != nil { + lib.Warning("can't register event %q: %s", EventCloud, err) + } + + process.Cast(process.Self(), messageCloudClientConnect{}) + + return nil +} + +func (cc *cloudClient) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + lib.Log("[%s] CLOUD_CLIENT: HandleCast: %#v", process.NodeName(), message) + switch message.(type) { + case messageCloudClientConnect: + state := process.State.(*cloudClientState) + + // initiate connection with the cloud + cloudNodes, err := getCloudNodes() + if err != nil { + lib.Warning("can't resolve cloud nodes: %s", err) + } + + // add static route with custom handshake + thisNode := process.Env(node.EnvKeyNode).(node.Node) + + for _, cloud := range cloudNodes { + routeOptions := node.RouteOptions{ + Cookie: state.options.Cookie, + IsErgo: true, + Handshake: state.handshake, + } + routeOptions.TLS = &tls.Config{ + InsecureSkipVerify: cloud.SkipVerify, + } + if err := thisNode.AddStaticRoutePort(cloud.Node, cloud.Port, routeOptions); err != nil { + if err != lib.ErrTaken { + continue + } + } + + if err := thisNode.Connect(cloud.Node); err != nil { + continue + } + + // add proxy domain route + proxyRoute := node.ProxyRoute{ + Name: "@" + state.options.Cluster, + Proxy: cloud.Node, + Cookie: state.options.Cookie, + } + thisNode.AddProxyRoute(proxyRoute) + + state.monitor = process.MonitorNode(cloud.Node) + state.node = cloud.Node + event := MessageEventCloud{ + Online: true, + } + process.SendEventMessage(EventCloud, event) + return gen.ServerStatusOK + } + + // cloud nodes aren't available. make another attempt in 3 seconds + process.CastAfter(process.Self(), messageCloudClientConnect{}, 5*time.Second) + } + return gen.ServerStatusOK +} + +func (cc *cloudClient) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + lib.Log("[%s] CLOUD_CLIENT: HandleInfo: %#v", process.NodeName(), message) + state := process.State.(*cloudClientState) + + switch m := message.(type) { + case gen.MessageDown: + if m.Ref != state.monitor { + return gen.ServerStatusOK + } + thisNode := process.Env(node.EnvKeyNode).(node.Node) + state.cleanup(thisNode) + + event := MessageEventCloud{ + Online: false, + } + process.SendEventMessage(EventCloud, event) + // lost connection with the cloud node. try to connect again + process.Cast(process.Self(), messageCloudClientConnect{}) + } + return gen.ServerStatusOK +} + +func (cc *cloudClient) Terminate(process *gen.ServerProcess, reason string) { + state := process.State.(*cloudClientState) + thisNode := process.Env(node.EnvKeyNode).(node.Node) + thisNode.RemoveProxyRoute("@" + state.options.Cluster) + thisNode.Disconnect(state.node) + state.cleanup(thisNode) +} + +func (ccs *cloudClientState) cleanup(node node.Node) { + node.RemoveStaticRoute(ccs.node) + node.RemoveProxyRoute("@" + ccs.options.Cluster) + ccs.node = "" +} + +func getCloudNodes() ([]CloudNode, error) { + // check if custom cloud entries have been defined via env + if entries := strings.Fields(os.Getenv("ERGO_SERVICES_CLOUD")); len(entries) > 0 { + nodes := []CloudNode{} + for _, entry := range entries { + re := regexp.MustCompile("[@:]+") + nameHostPort := re.Split(entry, -1) + name := "dist" + host := "localhost" + port := 4411 + switch len(nameHostPort) { + case 2: + // either abc@def or abc:def + if p, err := strconv.Atoi(nameHostPort[1]); err == nil { + port = p + } else { + name = nameHostPort[0] + host = nameHostPort[1] + } + case 3: + if p, err := strconv.Atoi(nameHostPort[2]); err == nil { + port = p + } else { + continue + } + name = nameHostPort[0] + host = nameHostPort[1] + + default: + continue + } + + node := CloudNode{ + Node: name + "@" + host, + Port: uint16(port), + SkipVerify: true, + } + nodes = append(nodes, node) + + } + + if len(nodes) > 0 { + return nodes, nil + } + } + _, srv, err := net.LookupSRV("cloud", "dist", "ergo.services") + if err != nil { + return nil, err + } + nodes := make([]CloudNode, len(srv)) + for i := range srv { + nodes[i].Node = "dist@" + strings.TrimSuffix(srv[i].Target, ".") + nodes[i].Port = srv[i].Port + } + return nodes, nil +} diff --git a/apps/cloud/handshake.go b/apps/cloud/handshake.go new file mode 100644 index 00000000..57606b4b --- /dev/null +++ b/apps/cloud/handshake.go @@ -0,0 +1,274 @@ +package cloud + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "fmt" + "hash" + "io" + "net" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +const ( + defaultHandshakeTimeout = 5 * time.Second + clusterNameLengthMax = 128 +) + +type Handshake struct { + node.Handshake + nodename string + creation uint32 + options node.Cloud + flags node.Flags +} + +type handshakeDetails struct { + cookieHash []byte + digestRemote []byte + details node.HandshakeDetails + mapName string + hash hash.Hash +} + +func createHandshake(options node.Cloud) (node.HandshakeInterface, error) { + if options.Timeout == 0 { + options.Timeout = defaultHandshakeTimeout + } + + if err := RegisterTypes(); err != nil { + return nil, err + } + + return &Handshake{ + options: options, + }, nil +} + +func (ch *Handshake) Init(nodename string, creation uint32, flags node.Flags) error { + if flags.EnableProxy == false { + s := "proxy feature must be enabled for the cloud connection" + lib.Warning(s) + return fmt.Errorf(s) + } + if ch.options.Cluster == "" { + s := "option Cloud.Cluster can not be empty" + lib.Warning(s) + return fmt.Errorf(s) + } + if len(ch.options.Cluster) > clusterNameLengthMax { + s := "option Cloud.Cluster has too long name" + lib.Warning(s) + return fmt.Errorf(s) + } + ch.nodename = nodename + ch.creation = creation + ch.flags = flags + if ch.options.Flags.Enable == false { + return nil + } + + ch.flags.EnableRemoteSpawn = ch.options.Flags.EnableRemoteSpawn + return nil +} + +func (ch *Handshake) Start(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (node.HandshakeDetails, error) { + hash := sha256.New() + handshake := &handshakeDetails{ + cookieHash: hash.Sum([]byte(cookie)), + hash: hash, + } + handshake.details.Flags = ch.flags + + ch.sendV1Auth(conn) + + // define timeout for the handshaking + timer := time.NewTimer(ch.options.Timeout) + defer timer.Stop() + + b := lib.TakeBuffer() + defer lib.ReleaseBuffer(b) + + asyncReadChannel := make(chan error, 2) + asyncRead := func() { + _, err := b.ReadDataFrom(conn, 1024) + asyncReadChannel <- err + } + + expectingBytes := 4 + await := []byte{ProtoHandshakeV1AuthReply, ProtoHandshakeV1Error} + + for { + go asyncRead() + select { + case <-timer.C: + return handshake.details, fmt.Errorf("timeout") + case err := <-asyncReadChannel: + if err != nil { + return handshake.details, err + } + + if b.Len() < expectingBytes { + continue + } + + if b.B[0] != ProtoHandshakeV1 { + return handshake.details, fmt.Errorf("malformed handshake proto") + } + + l := int(binary.BigEndian.Uint16(b.B[2:4])) + buffer := b.B[4 : l+4] + + if len(buffer) != l { + return handshake.details, fmt.Errorf("malformed handshake (wrong packet length)") + } + + // check if we got correct message type regarding to 'await' value + if bytes.Count(await, b.B[1:2]) == 0 { + return handshake.details, fmt.Errorf("malformed handshake sequence") + } + + await, err = ch.handle(conn, b.B[1], buffer, handshake) + if err != nil { + return handshake.details, err + } + + b.Reset() + } + + if await == nil { + // handshaked + break + } + } + + return handshake.details, nil +} + +func (ch *Handshake) handle(socket io.Writer, messageType byte, buffer []byte, details *handshakeDetails) ([]byte, error) { + switch messageType { + case ProtoHandshakeV1AuthReply: + if err := ch.handleV1AuthReply(buffer, details); err != nil { + return nil, err + } + if err := ch.sendV1Challenge(socket, details); err != nil { + return nil, err + } + return []byte{ProtoHandshakeV1ChallengeAccept, ProtoHandshakeV1Error}, nil + + case ProtoHandshakeV1ChallengeAccept: + if err := ch.handleV1ChallegeAccept(buffer, details); err != nil { + return nil, err + } + return nil, nil + + case ProtoHandshakeV1Error: + return nil, ch.handleV1Error(buffer) + + default: + return nil, fmt.Errorf("unknown message type") + } +} + +func (ch *Handshake) sendV1Auth(socket io.Writer) error { + b := lib.TakeBuffer() + defer lib.ReleaseBuffer(b) + + message := MessageHandshakeV1Auth{ + Node: ch.nodename, + Cluster: ch.options.Cluster, + Creation: ch.creation, + Flags: ch.options.Flags, + } + b.Allocate(1 + 1 + 2) + b.B[0] = ProtoHandshakeV1 + b.B[1] = ProtoHandshakeV1Auth + if err := etf.Encode(message, b, etf.EncodeOptions{}); err != nil { + return err + } + binary.BigEndian.PutUint16(b.B[2:4], uint16(b.Len()-4)) + if err := b.WriteDataTo(socket); err != nil { + return err + } + + return nil +} + +func (ch *Handshake) sendV1Challenge(socket io.Writer, handshake *handshakeDetails) error { + b := lib.TakeBuffer() + defer lib.ReleaseBuffer(b) + + digest := GenDigest(handshake.hash, []byte(ch.nodename), handshake.digestRemote, handshake.cookieHash) + message := MessageHandshakeV1Challenge{ + Digest: digest, + } + b.Allocate(1 + 1 + 2) + b.B[0] = ProtoHandshakeV1 + b.B[1] = ProtoHandshakeV1Challenge + if err := etf.Encode(message, b, etf.EncodeOptions{}); err != nil { + return err + } + binary.BigEndian.PutUint16(b.B[2:4], uint16(b.Len()-4)) + if err := b.WriteDataTo(socket); err != nil { + return err + } + + return nil + +} + +func (ch *Handshake) handleV1AuthReply(buffer []byte, handshake *handshakeDetails) error { + m, _, err := etf.Decode(buffer, nil, etf.DecodeOptions{}) + if err != nil { + return fmt.Errorf("malformed MessageHandshakeV1AuthReply message: %s", err) + } + message, ok := m.(MessageHandshakeV1AuthReply) + if ok == false { + return fmt.Errorf("malformed MessageHandshakeV1AuthReply message: %#v", m) + } + + digest := GenDigest(handshake.hash, []byte(message.Node), []byte(ch.options.Cluster), handshake.cookieHash) + if bytes.Compare(message.Digest, digest) != 0 { + return fmt.Errorf("wrong digest") + } + handshake.digestRemote = digest + handshake.details.Name = message.Node + handshake.details.Creation = message.Creation + + return nil +} + +func (ch *Handshake) handleV1ChallegeAccept(buffer []byte, handshake *handshakeDetails) error { + m, _, err := etf.Decode(buffer, nil, etf.DecodeOptions{}) + if err != nil { + return fmt.Errorf("malformed MessageHandshakeV1ChallengeAccept message: %s", err) + } + message, ok := m.(MessageHandshakeV1ChallengeAccept) + if ok == false { + return fmt.Errorf("malformed MessageHandshakeV1ChallengeAccept message: %#v", m) + } + + mapping := etf.NewAtomMapping() + mapping.In[etf.Atom(message.Node)] = etf.Atom(ch.nodename) + mapping.Out[etf.Atom(ch.nodename)] = etf.Atom(message.Node) + handshake.details.AtomMapping = mapping + handshake.mapName = message.Node + return nil +} + +func (ch *Handshake) handleV1Error(buffer []byte) error { + m, _, err := etf.Decode(buffer, nil, etf.DecodeOptions{}) + if err != nil { + return fmt.Errorf("malformed MessageHandshakeV1Error message: %s", err) + } + message, ok := m.(MessageHandshakeV1Error) + if ok == false { + return fmt.Errorf("malformed MessageHandshakeV1Error message: %#v", m) + } + return fmt.Errorf(message.Reason) +} diff --git a/apps/cloud/sup.go b/apps/cloud/sup.go new file mode 100644 index 00000000..847c6a4d --- /dev/null +++ b/apps/cloud/sup.go @@ -0,0 +1,28 @@ +package cloud + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type cloudAppSup struct { + gen.Supervisor +} + +func (cas *cloudAppSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { + return gen.SupervisorSpec{ + Children: []gen.SupervisorChildSpec{ + gen.SupervisorChildSpec{ + Name: "cloud_client", + Child: &cloudClient{}, + Args: args, + }, + }, + Strategy: gen.SupervisorStrategy{ + Type: gen.SupervisorStrategyOneForOne, + Intensity: 10, + Period: 5, + Restart: gen.SupervisorStrategyRestartPermanent, + }, + }, nil +} diff --git a/apps/cloud/types.go b/apps/cloud/types.go new file mode 100644 index 00000000..ce1271a7 --- /dev/null +++ b/apps/cloud/types.go @@ -0,0 +1,82 @@ +package cloud + +import ( + "hash" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +const ( + EventCloud gen.Event = "cloud" + + ProtoHandshakeV1 = 41 + ProtoHandshakeV1Auth = 100 + ProtoHandshakeV1AuthReply = 101 + ProtoHandshakeV1Challenge = 102 + ProtoHandshakeV1ChallengeAccept = 103 + ProtoHandshakeV1Error = 200 +) + +type MessageEventCloud struct { + Online bool +} + +func RegisterTypes() error { + types := []interface{}{ + node.CloudFlags{}, + MessageHandshakeV1Auth{}, + MessageHandshakeV1AuthReply{}, + MessageHandshakeV1Challenge{}, + MessageHandshakeV1ChallengeAccept{}, + MessageHandshakeV1Error{}, + } + rtOpts := etf.RegisterTypeOptions{Strict: true} + + for _, t := range types { + if _, err := etf.RegisterType(t, rtOpts); err != nil && err != lib.ErrTaken { + return err + } + } + return nil +} + +func GenDigest(h hash.Hash, items ...[]byte) []byte { + x := []byte{} + for _, i := range items { + x = append(x, i...) + } + return h.Sum(x) +} + +// client -> cloud +type MessageHandshakeV1Auth struct { + Node string + Cluster string + Creation uint32 + Flags node.CloudFlags +} + +// cloud -> client +type MessageHandshakeV1AuthReply struct { + Node string + Creation uint32 + Digest []byte +} + +// client -> cloud +type MessageHandshakeV1Challenge struct { + Digest []byte +} + +// cloud -> client +type MessageHandshakeV1ChallengeAccept struct { + Node string // mapped node name +} + +// cloud -> client +type MessageHandshakeV1Error struct { + Reason string +} diff --git a/erlang/appmon.go b/apps/erlang/appmon.go similarity index 100% rename from erlang/appmon.go rename to apps/erlang/appmon.go diff --git a/erlang/erlang.go b/apps/erlang/erlang.go similarity index 100% rename from erlang/erlang.go rename to apps/erlang/erlang.go diff --git a/erlang/global_name_server.go b/apps/erlang/global_name_server.go similarity index 100% rename from erlang/global_name_server.go rename to apps/erlang/global_name_server.go diff --git a/erlang/net_kernel.go b/apps/erlang/net_kernel.go similarity index 96% rename from erlang/net_kernel.go rename to apps/erlang/net_kernel.go index a36400ec..9fbc9990 100644 --- a/erlang/net_kernel.go +++ b/apps/erlang/net_kernel.go @@ -13,13 +13,17 @@ import ( "github.com/ergo-services/ergo/lib/osdep" ) +func CreateApp() gen.ApplicationBehavior { + return &kernelApp{} +} + // KernelApp -type KernelApp struct { +type kernelApp struct { gen.Application } // Load -func (nka *KernelApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { +func (nka *kernelApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { return gen.ApplicationSpec{ Name: "erlang", Description: "Erlang support app", @@ -34,7 +38,7 @@ func (nka *KernelApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { } // Start -func (nka *KernelApp) Start(p gen.Process, args ...etf.Term) {} +func (nka *kernelApp) Start(p gen.Process, args ...etf.Term) {} type netKernelSup struct { gen.Supervisor diff --git a/erlang/observer_backend.go b/apps/erlang/observer_backend.go similarity index 100% rename from erlang/observer_backend.go rename to apps/erlang/observer_backend.go diff --git a/erlang/rex.go b/apps/erlang/rex.go similarity index 94% rename from erlang/rex.go rename to apps/erlang/rex.go index 27db92b0..155ba371 100644 --- a/erlang/rex.go +++ b/apps/erlang/rex.go @@ -87,7 +87,7 @@ func (r *rex) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.Serve } // HandleDirect -func (r *rex) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (r *rex) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case gen.MessageManageRPC: mf := modFun{ @@ -97,21 +97,21 @@ func (r *rex) HandleDirect(process *gen.ServerProcess, message interface{}) (int // provide RPC if m.Provide { if _, ok := r.methods[mf]; ok { - return nil, node.ErrTaken + return nil, lib.ErrTaken } r.methods[mf] = m.Fun - return nil, nil + return nil, gen.DirectStatusOK } // revoke RPC if _, ok := r.methods[mf]; ok { delete(r.methods, mf) - return nil, nil + return nil, gen.DirectStatusOK } return nil, fmt.Errorf("unknown RPC name") default: - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } } diff --git a/apps/system/app.go b/apps/system/app.go new file mode 100644 index 00000000..46ac7340 --- /dev/null +++ b/apps/system/app.go @@ -0,0 +1,39 @@ +package system + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +func CreateApp(options node.System) gen.ApplicationBehavior { + return &systemApp{ + options: options, + } +} + +type systemApp struct { + gen.Application + options node.System +} + +func (sa *systemApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { + lib.Log("SYSTEM: Application load") + return gen.ApplicationSpec{ + Name: "system_app", + Description: "System Application", + Version: "v.1.0", + Children: []gen.ApplicationChildSpec{ + gen.ApplicationChildSpec{ + Child: &systemAppSup{}, + Name: "system_app_sup", + Args: []etf.Term{sa.options}, + }, + }, + }, nil +} + +func (sa *systemApp) Start(p gen.Process, args ...etf.Term) { + lib.Log("[%s] SYSTEM: Application started", p.NodeName()) +} diff --git a/apps/system/metrics.go b/apps/system/metrics.go new file mode 100644 index 00000000..0c66e228 --- /dev/null +++ b/apps/system/metrics.go @@ -0,0 +1,178 @@ +package system + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "net" + "runtime" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/lib/osdep" + "github.com/ergo-services/ergo/node" +) + +var ( + defaultMetricsPeriod = time.Minute +) + +type systemMetrics struct { + gen.Server +} + +type systemMetricsState struct { + // gather last 10 stats + stats [10]nodeFullStats + i int +} +type messageSystemAnonInfo struct{} +type messageSystemGatherStats struct{} + +type nodeFullStats struct { + timestamp int64 + utime int64 + stime int64 + + memAlloc uint64 + memTotalAlloc uint64 + memFrees uint64 + memSys uint64 + memNumGC uint32 + + node node.NodeStats + network []node.NetworkStats +} + +func (sb *systemMetrics) Init(process *gen.ServerProcess, args ...etf.Term) error { + lib.Log("[%s] SYSTEM_METRICS: Init: %#v", process.NodeName(), args) + if err := RegisterTypes(); err != nil { + return err + } + options := args[0].(node.System) + process.State = &systemMetricsState{} + if options.DisableAnonMetrics == false { + process.CastAfter(process.Self(), messageSystemAnonInfo{}, defaultMetricsPeriod) + } + process.CastAfter(process.Self(), messageSystemGatherStats{}, defaultMetricsPeriod) + return nil +} + +func (sb *systemMetrics) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + lib.Log("[%s] SYSTEM_METRICS: HandleCast: %#v", process.NodeName(), message) + state := process.State.(*systemMetricsState) + switch message.(type) { + case messageSystemAnonInfo: + ver := process.Env(node.EnvKeyVersion).(node.Version) + sendAnonInfo(process.NodeName(), ver) + + case messageSystemGatherStats: + stats := gatherStats(process) + if state.i > len(state.stats)-1 { + state.i = 0 + } + state.stats[state.i] = stats + state.i++ + process.CastAfter(process.Self(), messageSystemGatherStats{}, defaultMetricsPeriod) + } + return gen.ServerStatusOK +} + +func (sb *systemMetrics) Terminate(process *gen.ServerProcess, reason string) { + lib.Log("[%s] SYSTEM_METRICS: Terminate with reason %q", process.NodeName(), reason) +} + +// private routines + +func sendAnonInfo(name string, ver node.Version) { + metricsHost := "metrics.ergo.services" + + values, err := net.LookupTXT(metricsHost) + if err != nil || len(values) == 0 { + return + } + + v, err := base64.StdEncoding.DecodeString(values[0]) + if err != nil { + return + } + + pk, err := x509.ParsePKCS1PublicKey([]byte(v)) + if err != nil { + return + } + + c, err := net.Dial("udp", metricsHost+":4411") + if err != nil { + return + } + defer c.Close() + + // FIXME get it back before the release + // nameHash := crc32.Checksum([]byte(name), lib.CRC32Q) + nameHash := name + + b := lib.TakeBuffer() + defer lib.ReleaseBuffer(b) + + message := MessageSystemAnonMetrics{ + Name: nameHash, + Arch: runtime.GOARCH, + OS: runtime.GOOS, + NumCPU: runtime.NumCPU(), + GoVersion: runtime.Version(), + ErgoVersion: ver.Release, + } + if err := etf.Encode(message, b, etf.EncodeOptions{}); err != nil { + return + } + + hash := sha256.New() + cipher, err := rsa.EncryptOAEP(hash, rand.Reader, pk, b.B, nil) + if err != nil { + return + } + + // 2 (magic: 1144) + 2 (length) + len(cipher) + b.Reset() + b.Allocate(4) + b.Append(cipher) + binary.BigEndian.PutUint16(b.B[0:2], uint16(1144)) + binary.BigEndian.PutUint16(b.B[2:4], uint16(len(cipher))) + c.Write(b.B) +} + +func gatherStats(process *gen.ServerProcess) nodeFullStats { + fullStats := nodeFullStats{} + + // CPU (windows doesn't support this feature) + fullStats.utime, fullStats.stime = osdep.ResourceUsage() + + // Memory + mem := runtime.MemStats{} + runtime.ReadMemStats(&mem) + fullStats.memAlloc = mem.Alloc + fullStats.memTotalAlloc = mem.TotalAlloc + fullStats.memSys = mem.Sys + fullStats.memFrees = mem.Frees + fullStats.memNumGC = mem.NumGC + + // Network + node := process.Env(node.EnvKeyNode).(node.Node) + for _, name := range node.Nodes() { + ns, err := node.NetworkStats(name) + if err != nil { + continue + } + fullStats.network = append(fullStats.network, ns) + } + + fullStats.node = node.Stats() + fullStats.timestamp = time.Now().Unix() + return fullStats +} diff --git a/apps/system/sup.go b/apps/system/sup.go new file mode 100644 index 00000000..a36d5622 --- /dev/null +++ b/apps/system/sup.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type systemAppSup struct { + gen.Supervisor +} + +func (sas *systemAppSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { + return gen.SupervisorSpec{ + Children: []gen.SupervisorChildSpec{ + gen.SupervisorChildSpec{ + Name: "system_metrics", + Child: &systemMetrics{}, + Args: args, + }, + }, + Strategy: gen.SupervisorStrategy{ + Type: gen.SupervisorStrategyOneForOne, + Intensity: 10, + Period: 5, + Restart: gen.SupervisorStrategyRestartPermanent, + }, + }, nil +} diff --git a/apps/system/types.go b/apps/system/types.go new file mode 100644 index 00000000..7711e337 --- /dev/null +++ b/apps/system/types.go @@ -0,0 +1,29 @@ +package system + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type MessageSystemAnonMetrics struct { + Name string + Arch string + OS string + NumCPU int + GoVersion string + ErgoVersion string +} + +func RegisterTypes() error { + types := []interface{}{ + MessageSystemAnonMetrics{}, + } + rtOpts := etf.RegisterTypeOptions{Strict: true} + + for _, t := range types { + if _, err := etf.RegisterType(t, rtOpts); err != nil && err != lib.ErrTaken { + return err + } + } + return nil +} diff --git a/ergo.go b/ergo.go index 5d9655aa..f4145d38 100644 --- a/ergo.go +++ b/ergo.go @@ -3,8 +3,11 @@ package ergo import ( "context" - "github.com/ergo-services/ergo/erlang" + "github.com/ergo-services/ergo/apps/cloud" + "github.com/ergo-services/ergo/apps/erlang" + "github.com/ergo-services/ergo/apps/system" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" "github.com/ergo-services/ergo/proto/dist" ) @@ -26,8 +29,21 @@ func StartNodeWithContext(ctx context.Context, name string, cookie string, opts } opts.Env[node.EnvKeyVersion] = version - // add erlang support application - opts.Applications = append([]gen.ApplicationBehavior{&erlang.KernelApp{}}, opts.Applications...) + // add default applications: + defaultApps := []gen.ApplicationBehavior{ + system.CreateApp(opts.System), // system application (bus, metrics etc.) + erlang.CreateApp(), // erlang support + } + + // add cloud support if it's enabled + if opts.Cloud.Enable { + cloudApp := cloud.CreateApp(opts.Cloud) + defaultApps = append(defaultApps, cloudApp) + if opts.Proxy.Accept == false { + lib.Warning("Disabled option Proxy.Accept makes this node inaccessible to the other nodes within your cloud cluster, but it still allows initiate connection to the others with this option enabled.") + } + } + opts.Applications = append(defaultApps, opts.Applications...) if opts.Handshake == nil { // create default handshake for the node (Erlang Dist Handshake) @@ -40,9 +56,14 @@ func StartNodeWithContext(ctx context.Context, name string, cookie string, opts opts.Proto = dist.CreateProto(protoOptions) } - if opts.StaticRoutesOnly == false && opts.Resolver == nil { - // create default resolver (with enabled Erlang EPMD server) - opts.Resolver = dist.CreateResolverWithLocalEPMD("", dist.DefaultEPMDPort) + if opts.StaticRoutesOnly == false && opts.Registrar == nil { + // create default registrar (with enabled Erlang EPMD server) + opts.Registrar = dist.CreateRegistrarWithLocalEPMD("", dist.DefaultEPMDPort) + } + + if len(opts.Listeners) == 0 { + listener := node.DefaultListener() + opts.Listeners = append(opts.Listeners, listener) } return node.StartWithContext(ctx, name, cookie, opts) diff --git a/etf/cache.go b/etf/cache.go index 2cfb35e5..796d0b13 100644 --- a/etf/cache.go +++ b/etf/cache.go @@ -56,6 +56,21 @@ func NewAtomCache() AtomCache { } } +type AtomMapping struct { + MutexIn sync.RWMutex + In map[Atom]Atom + MutexOut sync.RWMutex + Out map[Atom]Atom +} + +// NewAtomMapping +func NewAtomMapping() AtomMapping { + return AtomMapping{ + In: make(map[Atom]Atom), + Out: make(map[Atom]Atom), + } +} + // Append func (a *AtomCacheOut) Append(atom Atom) (int16, bool) { a.Lock() diff --git a/etf/decode.go b/etf/decode.go index f2a5186d..bf8a9c08 100644 --- a/etf/decode.go +++ b/etf/decode.go @@ -5,20 +5,21 @@ import ( "fmt" "math" "math/big" + "reflect" "github.com/ergo-services/ergo/lib" ) // linked list for decoding complex types like list/map/tuple type stackElement struct { - parent *stackElement - - termType byte - - term Term //value - i int // current + parent *stackElement + reg *reflect.Value // used for registered types decoding + term Term //value + tmp Term // temporary value. used as a temporary storage for a key of map + i int // current children int - tmp Term // temporary value. uses as a temporary storage for a key of map + termType byte + strict bool // if encoding/decoding registered types must be strict } var ( @@ -59,6 +60,7 @@ var ( // DecodeOptions type DecodeOptions struct { + AtomMapping AtomMapping FlagBigPidRef bool } @@ -126,6 +128,14 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r if len([]rune(atom)) > 255 { return nil, nil, errMalformedAtomUTF8 } + + // replace atom value if we have mapped value for it + options.AtomMapping.MutexIn.RLock() + if mapped, ok := options.AtomMapping.In[atom]; ok { + atom = mapped + } + options.AtomMapping.MutexIn.RUnlock() + term = atom packet = packet[n+2:] @@ -145,7 +155,14 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r case "false": term = false default: - term = Atom(packet[1 : n+1]) + atom := Atom(packet[1 : n+1]) + // replace atom value if we have mapped value for it + options.AtomMapping.MutexIn.RLock() + if mapped, ok := options.AtomMapping.In[atom]; ok { + atom = mapped + } + options.AtomMapping.MutexIn.RUnlock() + term = atom } packet = packet[n+1:] @@ -173,7 +190,14 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r case "false": term = false default: - term = cache[int(packet[0])] + atom := cache[int(packet[0])] + // replace atom value if we have mapped value for it + options.AtomMapping.MutexIn.RLock() + if mapped, ok := options.AtomMapping.In[atom]; ok { + atom = mapped + } + options.AtomMapping.MutexIn.RUnlock() + term = atom } packet = packet[1:] @@ -493,8 +517,50 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r // Stage 2 processStack: if stack != nil { + var field reflect.Value + var set_field bool + switch stack.termType { case ettList: + if stack.i == 0 { + // if the first value is atom, check for the registered type + if typeName, isAtom := term.(Atom); isAtom == true { + registered.RLock() + r, found := registered.typesDec[typeName] + registered.RUnlock() + if found == true { + reg := reflect.Indirect(reflect.New(r.rtype)) + if r.rtype.Kind() == reflect.Slice { + reg = reflect.MakeSlice(r.rtype, stack.children-2, stack.children-1) + } + stack.reg = ® + stack.strict = r.strict + if r.strict == false { + stack.term.(List)[stack.i] = term + } + stack.i++ + break + } + } + } + + if stack.reg != nil { + if stack.i+1 == stack.children { + if t != ettNil { + x := reflect.Append(*stack.reg, reflect.ValueOf(term)) + stack.reg = &x + } + } else { + set_field = true + field = stack.reg.Index(stack.i - 1) + } + + if stack.strict == true { + stack.i++ + break + } + } + stack.term.(List)[stack.i] = term stack.i++ // remove the last element for proper list (its ettNil) @@ -503,11 +569,71 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r } case ettSmallTuple, ettLargeTuple: + + if stack.i == 0 { + // if the first value is atom, check for the registered type + if typeName, isAtom := term.(Atom); isAtom == true { + registered.RLock() + r, found := registered.typesDec[typeName] + registered.RUnlock() + if found == true { + reg := reflect.Indirect(reflect.New(r.rtype)) + stack.reg = ® + stack.strict = r.strict + if r.strict == false { + stack.term.(Tuple)[stack.i] = term + } + stack.i++ + break + } + } + } + + if stack.reg != nil { + set_field = true + field = stack.reg.Field(stack.i - 1) + if stack.strict == true { + stack.i++ + break + } + } stack.term.(Tuple)[stack.i] = term stack.i++ case ettMap: + if stack.i == 0 { + // if the first key is atom, check for the registered type + if typeName, isAtom := term.(Atom); isAtom == true { + registered.RLock() + r, found := registered.typesDec[typeName] + registered.RUnlock() + if found == true { + reg := reflect.MakeMapWithSize(r.rtype, stack.children/2) + stack.reg = ® + stack.strict = r.strict + if r.strict == false { + if stack.i&0x01 == 0x01 { // a value + stack.term.(Map)[stack.tmp] = term + } else { + stack.tmp = term + } + } + stack.i++ + break + } + } + } + if stack.i == 1 && stack.reg != nil && stack.strict == true { + // skip it. the value of the key which is the registered type + stack.i++ + break + + } if stack.i&0x01 == 0x01 { // a value + if stack.i > 1 && stack.reg != nil { + set_field = true + field = *stack.reg + } stack.term.(Map)[stack.tmp] = term stack.i++ break @@ -781,6 +907,410 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r default: return nil, nil, errInternal } + + if set_field { + if field.Kind() == reflect.Ptr { + pfield := reflect.New(field.Type().Elem()) + field.Set(pfield) + field = pfield.Elem() + } + switch field.Kind() { + case reflect.Int8: + switch v := term.(type) { + case int: + if v > math.MaxInt8 || v < math.MinInt8 { + // overflows + if stack.strict { + panic("overflows int8") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + case int64: + if v > math.MaxInt8 || v < math.MinInt8 { + // overflows + if stack.strict { + panic("overflows int8") + } + stack.reg = nil + break + } + field.SetInt(v) + case uint64: + if v > math.MaxInt8 { + // overflows + if stack.strict { + panic("overflows int8") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + } + + case reflect.Int16: + switch v := term.(type) { + case int: + if v > math.MaxInt16 || v < math.MinInt16 { + // overflows + if stack.strict { + panic("overflows int16") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + case int64: + if v > math.MaxInt16 || v < math.MinInt16 { + // overflows + if stack.strict { + panic("overflows int16") + } + stack.reg = nil + break + } + field.SetInt(v) + case uint64: + if v > math.MaxInt16 { + // overflows + if stack.strict { + panic("overflows int16") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + } + + case reflect.Int32: + switch v := term.(type) { + case int: + if v > math.MaxInt32 || v < math.MinInt32 { + // overflows + if stack.strict { + panic("overflows int32") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + case int64: + if v > math.MaxInt32 || v < math.MinInt32 { + // overflows + if stack.strict { + panic("overflows int32") + } + stack.reg = nil + break + } + field.SetInt(v) + case uint64: + if v > math.MaxInt32 { + // overflows + if stack.strict { + panic("overflows int32") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + } + case reflect.Int64: + switch v := term.(type) { + case int: + field.SetInt(int64(v)) + case int64: + field.SetInt(v) + case uint64: + if v > math.MaxInt64 { + // overflows + if stack.strict { + panic("overflows int64") + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + } + case reflect.Int: + switch v := term.(type) { + case int: + field.SetInt(int64(v)) + case int64: + if v > math.MaxInt { + // overflows + if stack.strict { + panic("overflows int") + } + stack.reg = nil + break + } + field.SetInt(v) + case uint64: + if v > math.MaxInt { + // overflows + if stack.strict { + panic("overflows int") + return nil, nil, errMalformed + } + stack.reg = nil + break + } + field.SetInt(int64(v)) + } + + case reflect.Uint8: + switch v := term.(type) { + case int: + if int64(v) > math.MaxUint8 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint8") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case int64: + if v > math.MaxUint8 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint8") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case uint64: + if v > math.MaxUint8 { + // overflows + if stack.strict { + panic("overflows uint8") + } + stack.reg = nil + break + } + field.SetUint(v) + } + + case reflect.Uint16: + switch v := term.(type) { + case int: + if int64(v) > math.MaxUint16 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint16") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case int64: + if v > math.MaxUint16 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint16") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case uint64: + if v > math.MaxUint16 { + // overflows + if stack.strict { + panic("overflows uint16") + } + stack.reg = nil + break + } + field.SetUint(v) + } + case reflect.Uint32: + switch v := term.(type) { + case int: + if int64(v) > math.MaxUint32 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint32") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case int64: + if v > math.MaxUint32 || v < 0 { + // overflows + if stack.strict { + panic("overflows uint32") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case uint64: + if v > math.MaxUint32 { + // overflows + if stack.strict { + panic("overflows uint32") + } + stack.reg = nil + break + } + field.SetUint(v) + } + + case reflect.Uint64: + switch v := term.(type) { + case int: + if v < 0 { + // overflows + if stack.strict { + panic("overflows uint64") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case int64: + if v < 0 { + // overflows + if stack.strict { + panic("overflows uint64") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case uint64: + field.SetUint(v) + } + + case reflect.Uint: + switch v := term.(type) { + case int: + if v < 0 { + // overflows + if stack.strict { + panic("overflows uint") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case int64: + if v > math.MaxInt || v < 0 { + // overflows + if stack.strict { + panic("overflows uint") + } + stack.reg = nil + break + } + field.SetUint(uint64(v)) + case uint64: + if v > math.MaxUint { + // overflows + if stack.strict { + panic("overflows uint") + } + stack.reg = nil + break + } + field.SetUint(v) + } + case reflect.Float32: + f, ok := term.(float64) + if ok == false { + if stack.strict { + panic("wrong float value") + } + stack.reg = nil + break + } + field.SetFloat(f) + + case reflect.Float64: + f64, ok := term.(float64) + if ok == false { + if stack.strict { + panic("wrong float64") + } + stack.reg = nil + break + } + field.SetFloat(f64) + + case reflect.String: + switch v := term.(type) { + case List: + s, err := convertCharlistToString(v) + if err != nil { + if stack.strict { + panic("can't convert charlist into string") + } + stack.reg = nil + break + } + field.SetString(s) + case []byte: + field.SetString(string(v)) + case string: + field.SetString(v) + case Atom: + field.SetString(string(v)) + default: + if stack.strict { + panic("wrong string value") + } + stack.reg = nil + } + case reflect.Bool: + b, ok := term.(bool) + if !ok { + if stack.strict { + panic("wrong bool value") + } + stack.reg = nil + break + } + field.SetBool(b) + + case reflect.Map: + if stack.tmp == nil { + field.Set(reflect.ValueOf(term)) + break + } + destkey := reflect.ValueOf(stack.tmp) + destval := reflect.ValueOf(term) + stack.reg.SetMapIndex(destkey, destval) + + default: + if stack.strict { + field.Set(reflect.ValueOf(term)) + } else { + // wrap it to catch the panic + setValue := func(f reflect.Value, v interface{}) (ok bool) { + if lib.CatchPanic() { + defer func() { + if r := recover(); r != nil { + ok = false + } + }() + } + f.Set(reflect.ValueOf(v)) + return true + } + if setValue(field, term) == false { + stack.reg = nil + } + + } + } + + } + } // we are still decoding children of Lis/Map/Tuple/... @@ -788,7 +1318,11 @@ func Decode(packet []byte, cache []Atom, options DecodeOptions) (retTerm Term, r continue } - term = stack.term + if stack.reg != nil { + term = (*stack.reg).Interface() + } else { + term = stack.term + } // this term was the last element of List/Map/Tuple/... // pop from the stack, but if its the root just finish diff --git a/etf/decode_test.go b/etf/decode_test.go index 25aaacc1..1985116e 100644 --- a/etf/decode_test.go +++ b/etf/decode_test.go @@ -1,6 +1,7 @@ package etf import ( + "fmt" "math/big" "reflect" "testing" @@ -481,6 +482,47 @@ func TestDecodeFunction(t *testing.T) { } +func TestDecodeRegisteredType(t *testing.T) { + type regTypeStruct3 struct { + C string + } + type regTypeStruct4 struct { + A uint8 + B *regTypeStruct3 + } + if a, err := RegisterType(regTypeStruct3{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + if a, err := RegisterType(regTypeStruct4{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + expected := regTypeStruct4{} + expected.A = 123 + expected.B = ®TypeStruct3{ + C: "hello", + } + + packet := []byte{ettSmallTuple, 3, ettSmallAtomUTF8, 49, 35, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 47, 101, 114, 103, 111, 45, 115, 101, 114, 118, 105, 99, 101, 115, 47, 101, 114, 103, 111, 47, 101, 116, 102, 47, 114, 101, 103, 84, 121, 112, 101, 83, 116, 114, 117, 99, 116, 52, ettSmallInteger, 123, ettSmallTuple, 2, ettSmallAtomUTF8, 49, 35, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 47, 101, 114, 103, 111, 45, 115, 101, 114, 118, 105, 99, 101, 115, 47, 101, 114, 103, 111, 47, 101, 116, 102, 47, 114, 101, 103, 84, 121, 112, 101, 83, 116, 114, 117, 99, 116, 51, ettString, 0, 5, 104, 101, 108, 108, 111} + + term, _, err := Decode(packet, []Atom{}, DecodeOptions{}) + if err != nil { + t.Fatal(err) + } + switch tt := term.(type) { + case regTypeStruct4: + fmt.Printf("TERM: %v %#v %#v\n", tt, tt, tt.B) + default: + t.Fatal("unknown type", tt) + } + +} + // // benchmarks // diff --git a/etf/encode.go b/etf/encode.go index 85dad0d1..248c999c 100644 --- a/etf/encode.go +++ b/etf/encode.go @@ -14,9 +14,13 @@ var ( ErrStringTooLong = fmt.Errorf("Encoding error. String too long. Max allowed length is 65535") ErrAtomTooLong = fmt.Errorf("Encoding error. Atom too long. Max allowed UTF-8 chars is 255") - goSlice = byte(240) // internal type - goMap = byte(241) // internal type - goStruct = byte(242) // internal type + // internal types + goSlice = byte(240) + goMap = byte(241) + goStruct = byte(242) + goSliceRegistered = byte(243) + goMapRegistered = byte(244) + goStructRegistered = byte(245) marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() ) @@ -26,6 +30,7 @@ type EncodeOptions struct { AtomCache *AtomCacheOut SenderAtomCache map[Atom]CacheItem EncodingAtomCache *EncodingAtomCache + AtomMapping AtomMapping // FlagBigPidRef The node accepts a larger amount of data in pids // and references (node container types version 4). @@ -222,6 +227,22 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { } term = key + case goMapRegistered: + if stack.i == 0 { // registered type name as a key + term = stack.tmp + break + } + if stack.i == 1 { // nil as a value for the key (registered type name) + term = nil + break + } + key := stack.term.([]reflect.Value)[(stack.i-2)/2] + if stack.i&0x01 == 0x01 { // a value + term = stack.reg.MapIndex(key).Interface() + break + } + term = key.Interface() // a key + case goMap: key := stack.tmp.([]reflect.Value)[stack.i/2] if stack.i&0x01 == 0x01 { // a value @@ -230,6 +251,18 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { } term = key.Interface() // a key + case goSliceRegistered: + if stack.i == 0 { + term = stack.tmp + break + } + if stack.i == stack.children-1 { + // last item of list should be ettNil + term = nil + break + } + term = stack.term.(func(int) reflect.Value)(stack.i - 1).Interface() + case goSlice: if stack.i == stack.children-1 { // last item of list should be ettNil @@ -238,6 +271,15 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { } term = stack.term.(func(int) reflect.Value)(stack.i).Interface() + case goStructRegistered: + if stack.i == 0 { + // first item must be a sturct name (stored in stack.tmp). + term = stack.tmp + break + } + // field value + term = stack.term.(func(int) reflect.Value)(stack.i - 1).Interface() + case goStruct: field := stack.tmp.(func(int) reflect.StructField)(stack.i / 2) fieldName := field.Name @@ -252,7 +294,11 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { } // a value - term = stack.term.(func(int) reflect.Value)(stack.i / 2).Interface() + fvalue := stack.term.(func(int) reflect.Value)(stack.i / 2) + if fvalue.CanInterface() == false { + return fmt.Errorf("struct has unexported field %q", fieldName) + } + term = fvalue.Interface() default: @@ -510,6 +556,13 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { // characters and are always encoded using the UTF-8 external // formats ATOM_UTF8_EXT or SMALL_ATOM_UTF8_EXT. + // replace atom value if we have mapped value for it + options.AtomMapping.MutexOut.RLock() + if mapped, ok := options.AtomMapping.Out[t]; ok { + t = mapped + } + options.AtomMapping.MutexOut.RUnlock() + // https://erlang.org/doc/apps/erts/erl_ext_dist.html#utf8_atoms // The maximum number of allowed characters in an atom is 255. // In the UTF-8 case, each character can need 4 bytes to be encoded. @@ -694,10 +747,36 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { default: v := reflect.ValueOf(t) + vt := reflect.TypeOf(t) + vtAtomName := regTypeName(vt) + registered.RLock() + rtype, typeIsRegistered := registered.typesEnc[vtAtomName] + registered.RUnlock() switch v.Kind() { case reflect.Struct: lenStruct := v.NumField() + if typeIsRegistered { + // registered type. encode as a tuple with vtAtomName as the first element + vtAtomName = rtype.name + if lenStruct+1 < 255 { + b.Append([]byte{ettSmallTuple, byte(lenStruct + 1)}) + } else { + buf := b.Extend(5) + buf[0] = ettLargeTuple + binary.BigEndian.PutUint32(buf[1:], uint32(lenStruct+1)) + } + child = &stackElement{ + parent: stack, + termType: goStructRegistered, + term: v.Field, + children: lenStruct + 1, + tmp: vtAtomName, + } + break + } + + // will be encoded as a ettMap buf := b.Extend(5) buf[0] = ettMap binary.BigEndian.PutUint32(buf[1:], uint32(lenStruct)) @@ -712,10 +791,28 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { case reflect.Array, reflect.Slice: lenList := v.Len() + + if typeIsRegistered { + vtAtomName = rtype.name + lenList++ // first element for the type name + buf := b.Extend(5) + buf[0] = ettList + binary.BigEndian.PutUint32(buf[1:], uint32(lenList)) + child = &stackElement{ + parent: stack, + termType: goSliceRegistered, + term: v.Index, + children: lenList + 1, + tmp: vtAtomName, + } + break + } + if lenList == 0 { b.AppendByte(ettNil) continue } + buf := b.Extend(5) buf[0] = ettList binary.BigEndian.PutUint32(buf[1:], uint32(lenList)) @@ -728,6 +825,24 @@ func Encode(term Term, b *lib.Buffer, options EncodeOptions) (retErr error) { case reflect.Map: lenMap := v.Len() + if typeIsRegistered { + lenMap++ + vtAtomName = rtype.name + buf := b.Extend(5) + buf[0] = ettMap + binary.BigEndian.PutUint32(buf[1:], uint32(lenMap)) + + child = &stackElement{ + parent: stack, + termType: goMapRegistered, + term: v.MapKeys(), + children: lenMap * 2, + tmp: vtAtomName, + reg: &v, + } + break + } + buf := b.Extend(5) buf[0] = ettMap binary.BigEndian.PutUint32(buf[1:], uint32(lenMap)) diff --git a/etf/encode_test.go b/etf/encode_test.go index 8a59627d..83288450 100644 --- a/etf/encode_test.go +++ b/etf/encode_test.go @@ -913,6 +913,87 @@ func TestEncodeGoPtrNil(t *testing.T) { } } +func TestEncodeRegisteredType(t *testing.T) { + var tmp int + type regTypeStruct1 struct { + a int + } + type regTypeStruct3 struct { + C string + } + type regTypeStruct2 struct { + A int + B regTypeStruct3 + } + type regTypeMap map[string]regTypeStruct3 + type regTypeSlice []regTypeStruct3 + type regTypeArray [5]regTypeStruct3 + + // only struct/map/slice/array types are supported + if _, err := RegisterType(tmp, RegisterTypeOptions{}); err == nil { + t.Fatal("must be error here") + } + + // only struct with no unexported fields + if _, err := RegisterType(regTypeStruct1{}, RegisterTypeOptions{}); err == nil { + t.Fatal("must be error here") + } + + // all nested struct must be registered first + if _, err := RegisterType(regTypeStruct2{}, RegisterTypeOptions{}); err == nil { + t.Fatal("must be error here") + } + + if a, err := RegisterType(regTypeStruct3{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + if a, err := RegisterType(regTypeStruct2{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + if a, err := RegisterType(regTypeMap{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + if a, err := RegisterType(regTypeSlice{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + if a, err := RegisterType(regTypeArray{}, RegisterTypeOptions{}); err != nil { + t.Fatal(err) + } else { + defer UnregisterType(a) + } + + b := lib.TakeBuffer() + defer lib.ReleaseBuffer(b) + + x := regTypeStruct2{} + x.A = 123 + x.B.C = "hello" + + expected := []byte{ettSmallTuple, 3, ettSmallAtomUTF8, 49, 35, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 47, 101, 114, 103, 111, 45, 115, 101, 114, 118, 105, 99, 101, 115, 47, 101, 114, 103, 111, 47, 101, 116, 102, 47, 114, 101, 103, 84, 121, 112, 101, 83, 116, 114, 117, 99, 116, 50, ettSmallInteger, 123, ettSmallTuple, 2, ettSmallAtomUTF8, 49, 35, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 47, 101, 114, 103, 111, 45, 115, 101, 114, 118, 105, 99, 101, 115, 47, 101, 114, 103, 111, 47, 101, 116, 102, 47, 114, 101, 103, 84, 121, 112, 101, 83, 116, 114, 117, 99, 116, 51, ettString, 0, 5, 104, 101, 108, 108, 111} + err := Encode(x, b, EncodeOptions{}) + + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(b.B, expected) { + fmt.Println("exp", expected) + fmt.Println("got", b.B) + t.Fatal("incorrect value") + } +} + type testMarshal struct{} func (testMarshal) MarshalETF() ([]byte, error) { diff --git a/etf/etf.go b/etf/etf.go index aa29c93d..3ecb4c62 100644 --- a/etf/etf.go +++ b/etf/etf.go @@ -5,8 +5,76 @@ import ( "hash/crc32" "reflect" "strings" + "sync" + + "github.com/ergo-services/ergo/lib" +) + +var ( + registered = registeredTypes{ + typesEnc: make(map[Atom]*registerType), + typesDec: make(map[Atom]*registerType), + } ) +// Erlang external term tags. +const ( + ettAtom = byte(100) //deprecated + ettAtomUTF8 = byte(118) + ettSmallAtom = byte(115) //deprecated + ettSmallAtomUTF8 = byte(119) + ettString = byte(107) + + ettCacheRef = byte(82) + + ettNewFloat = byte(70) + + ettSmallInteger = byte(97) + ettInteger = byte(98) + ettLargeBig = byte(111) + ettSmallBig = byte(110) + + ettList = byte(108) + ettListImproper = byte(18) // to be able to encode improper lists like [a|b]. + ettSmallTuple = byte(104) + ettLargeTuple = byte(105) + + ettMap = byte(116) + + ettBinary = byte(109) + ettBitBinary = byte(77) + + ettNil = byte(106) + + ettPid = byte(103) + ettNewPid = byte(88) // since OTP 23, only when BIG_CREATION flag is set + ettNewRef = byte(114) + ettNewerRef = byte(90) // since OTP 21, only when BIG_CREATION flag is set + + ettExport = byte(113) + ettFun = byte(117) // legacy, wont support it here + ettNewFun = byte(112) + + ettPort = byte(102) + ettNewPort = byte(89) // since OTP 23, only when BIG_CREATION flag is set + + // ettRef = byte(101) deprecated + + ettFloat = byte(99) // legacy +) + +type registeredTypes struct { + sync.RWMutex + typesEnc map[Atom]*registerType + typesDec map[Atom]*registerType +} +type registerType struct { + rtype reflect.Type + name Atom + origin Atom + strict bool +} + // Term type Term interface{} @@ -98,10 +166,6 @@ type Function struct { FreeVars []Term } -var ( - crc32q = crc32.MakeTable(0xD5828281) -) - // Export type Export struct { Module Atom @@ -109,52 +173,6 @@ type Export struct { Arity int } -// Erlang external term tags. -const ( - ettAtom = byte(100) //deprecated - ettAtomUTF8 = byte(118) - ettSmallAtom = byte(115) //deprecated - ettSmallAtomUTF8 = byte(119) - ettString = byte(107) - - ettCacheRef = byte(82) - - ettNewFloat = byte(70) - - ettSmallInteger = byte(97) - ettInteger = byte(98) - ettLargeBig = byte(111) - ettSmallBig = byte(110) - - ettList = byte(108) - ettListImproper = byte(18) // to be able to encode improper lists like [a|b]. - ettSmallTuple = byte(104) - ettLargeTuple = byte(105) - - ettMap = byte(116) - - ettBinary = byte(109) - ettBitBinary = byte(77) - - ettNil = byte(106) - - ettPid = byte(103) - ettNewPid = byte(88) // since OTP 23, only when BIG_CREATION flag is set - ettNewRef = byte(114) - ettNewerRef = byte(90) // since OTP 21, only when BIG_CREATION flag is set - - ettExport = byte(113) - ettFun = byte(117) // legacy, wont support it here - ettNewFun = byte(112) - - ettPort = byte(102) - ettNewPort = byte(89) // since OTP 23, only when BIG_CREATION flag is set - - // ettRef = byte(101) deprecated - - ettFloat = byte(99) // legacy -) - // Element func (m Map) Element(k Term) Term { return m[k] @@ -179,7 +197,7 @@ func (p Pid) String() string { n := uint32(0) if p.Node != "" { - n = crc32.Checksum([]byte(p.Node), crc32q) + n = crc32.Checksum([]byte(p.Node), lib.CRC32Q) } return fmt.Sprintf("<%08X.%d.%d>", n, int32(p.ID>>32), int32(p.ID)) } @@ -188,7 +206,7 @@ func (p Pid) String() string { func (r Ref) String() string { n := uint32(0) if r.Node != "" { - n = crc32.Checksum([]byte(r.Node), crc32q) + n = crc32.Checksum([]byte(r.Node), lib.CRC32Q) } return fmt.Sprintf("Ref#<%08X.%d.%d.%d>", n, r.ID[0], r.ID[1], r.ID[2]) } @@ -197,7 +215,7 @@ func (r Ref) String() string { func (a Alias) String() string { n := uint32(0) if a.Node != "" { - n = crc32.Checksum([]byte(a.Node), crc32q) + n = crc32.Checksum([]byte(a.Node), lib.CRC32Q) } return fmt.Sprintf("Ref#<%08X.%d.%d.%d>", n, a.ID[0], a.ID[1], a.ID[2]) } @@ -637,6 +655,129 @@ func setMapMapField(term Map, dest reflect.Value) error { return nil } +// RegisterTypeOptins defines custom name for the registering type. +// Leaving the Name option empty makes the name automatically generated. +// Strict option defines whether the decoding process causes panic +// if the decoding value doesn't fit the destination object. +type RegisterTypeOptions struct { + Name Atom + Strict bool +} + +// RegisterType registers new type with the given options. It returns a Name +// of the registered type, which can be used in the UnregisterType function +// for unregistering this type. +// Returns an error if this type can not be registered. +func RegisterType(t interface{}, options RegisterTypeOptions) (Atom, error) { + tt := reflect.TypeOf(t) + ttk := tt.Kind() + + name := options.Name + origin := regTypeName(tt) + if name == "" { + name = origin + } + lname := len([]rune(name)) + if lname > 255 { + return name, fmt.Errorf("type name %q is too long. characters number %d (limit: 255)", name, lname) + } + + switch ttk { + case reflect.Struct, reflect.Slice, reflect.Array: + case reflect.Map: + // Using pointers for the network messaging is meaningless. + // Supporting this feature in the maps is getting the decoding process a bit overloaded. + // But they still can be used for the other types, even being meaningless. + if tt.Key().Kind() == reflect.Ptr { + return name, fmt.Errorf("pointer as a key for the map is not supported") + } + if tt.Elem().Kind() == reflect.Ptr { + return name, fmt.Errorf("pointer as a value for the map is not supported") + } + // supported types + default: + return name, fmt.Errorf("type %q is not supported", regTypeName(tt)) + } + + registered.Lock() + defer registered.Unlock() + + _, taken := registered.typesDec[name] + if taken { + return name, lib.ErrTaken + } + + r, taken := registered.typesEnc[origin] + if taken { + return name, fmt.Errorf("type is already registered as %q", r.name) + } + + checkIsRegistered := func(name Atom, rt reflect.Kind) error { + switch rt { + case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map: + // check if this type is registered + _, taken := registered.typesEnc[name] + if taken == false { + return fmt.Errorf("type %q must be registered first", name) + } + case reflect.Chan, reflect.Func, reflect.UnsafePointer, reflect.Complex64, reflect.Complex128: + return fmt.Errorf("type %q is not supported", rt) + } + return nil + } + + switch ttk { + case reflect.Struct: + // check for unexported fields + tv := reflect.ValueOf(t) + for i := 0; i < tv.NumField(); i++ { + f := tv.Field(i) + if f.CanInterface() == false { + return name, fmt.Errorf("struct has unexported field(s)") + } + + if f.Type().Kind() == reflect.Slice && f.Type().Elem().Kind() == reflect.Uint8 { + // []byte + continue + } + + orig := regTypeName(f.Type()) + if err := checkIsRegistered(orig, f.Kind()); err != nil { + return name, err + } + } + case reflect.Array, reflect.Slice, reflect.Map: + elem := tt.Elem() + orig := regTypeName(elem) + if err := checkIsRegistered(orig, elem.Kind()); err != nil { + return name, err + } + } + + rt := ®isterType{ + rtype: reflect.TypeOf(t), + name: name, + origin: origin, + strict: options.Strict, + } + registered.typesEnc[origin] = rt + registered.typesDec[name] = rt + return name, nil +} + +// UnregisterType unregisters type with a given name. +func UnregisterType(name Atom) error { + registered.Lock() + defer registered.Unlock() + r, found := registered.typesDec[name] + if found == false { + return lib.ErrUnknown + } + delete(registered.typesDec, name) + delete(registered.typesEnc, r.origin) + return nil +} + type StructPopulatorError struct { Type reflect.Type Term Term @@ -681,3 +822,7 @@ func convertCharlistToString(l List) (string, error) { } return string(runes), nil } + +func regTypeName(t reflect.Type) Atom { + return Atom("#" + t.PkgPath() + "/" + t.Name()) +} diff --git a/etf/etf_test.go b/etf/etf_test.go index 90ad888b..6cb9ebe3 100644 --- a/etf/etf_test.go +++ b/etf/etf_test.go @@ -584,3 +584,107 @@ func TestTermIntoStructUnmarshal(t *testing.T) { t.Errorf("got %v, want %v", dst1, src1) } } + +func TestRegisterType(t *testing.T) { + type ccc []string + type ddd [3]bool + type aaa struct { + A string + B int + B8 int8 + B16 int16 + B32 int32 + B64 int64 + + BU uint + BU8 uint8 + BU16 uint16 + BU32 uint32 + BU64 uint64 + + C float32 + C64 float64 + D ddd + } + type bbb map[aaa]ccc + + src := bbb{ + aaa{ + A: "aa", + B: -11, + B8: -18, + B16: -1116, + B32: -1132, + B64: -1164, + BU: 0xb, + BU8: 0x12, + BU16: 0x45c, + BU32: 0x46c, + BU64: 0x48c, + C: -11.22, + C64: 1164.22, + D: ddd{true, false, false}}: ccc{"a1", "a2", "a3"}, + aaa{ + A: "bb", + B: -22, + B8: -28, + B16: -2216, + B32: -2232, + B64: -2264, + BU: 0x16, + BU8: 0x1c, + BU16: 0x8a8, + BU32: 0x8b8, + BU64: 0x8d8, + C: -22.22, + C64: 2264.22, + D: ddd{false, true, false}}: ccc{"b1", "b2", "b3"}, + aaa{ + A: "cc", + B: -33, + B8: -38, + B16: -3316, + B32: -3332, + B64: -3364, + BU: 0x21, + BU8: 0x26, + BU16: 0xcf4, + BU32: 0xd04, + BU64: 0xd24, + C: -33.22, + C64: 3364.22, + D: ddd{false, false, true}}: ccc{}, //test empty list + } + + buf := lib.TakeBuffer() + defer lib.ReleaseBuffer(buf) + + if _, err := RegisterType(ddd{}, RegisterTypeOptions{Strict: true}); err != nil { + t.Fatal(err) + } + if _, err := RegisterType(aaa{}, RegisterTypeOptions{Strict: true}); err != nil { + t.Fatal(err) + } + if _, err := RegisterType(ccc{}, RegisterTypeOptions{Strict: true}); err != nil { + t.Fatal(err) + } + if _, err := RegisterType(bbb{}, RegisterTypeOptions{Strict: true}); err != nil { + t.Fatal(err) + } + + if err := Encode(src, buf, EncodeOptions{}); err != nil { + t.Fatal(err) + } + dst, _, err := Decode(buf.B, []Atom{}, DecodeOptions{}) + if err != nil { + t.Fatal(err) + } + + if _, ok := dst.(bbb); !ok { + t.Fatal("wrong term result") + } + + if !reflect.DeepEqual(src, dst) { + t.Errorf("got %v, want %v", dst, src) + } +} diff --git a/examples/application/README.md b/examples/application/README.md new file mode 100644 index 00000000..9bfda676 --- /dev/null +++ b/examples/application/README.md @@ -0,0 +1,45 @@ +## Application demo scenario ## + +This example implements a simple application that starts one server and one supervisor. Here is supervision tree of this application + +``` + demoApp + | + - demoSup + | | + | - demoServer01 + | - demoServer02 + | - demoServer03 + | + - demoServer +``` + +Here is output of this example: +``` +❯❯❯❯ go run . + +to stop press Ctrl-C + +Started new process + Pid: <1A1F7F53.0.1013> + Name: "demoServer01" + Parent: <1A1F7F53.0.1012> + Args:[]etf.Term(nil) +Started new process + Pid: <1A1F7F53.0.1014> + Name: "demoServer02" + Parent: <1A1F7F53.0.1012> + Args:[]etf.Term{12345} +Started new process + Pid: <1A1F7F53.0.1015> + Name: "demoServer03" + Parent: <1A1F7F53.0.1012> + Args:[]etf.Term{"abc", 67890} +Started new process + Pid: <1A1F7F53.0.1016> + Name: "demoServer" + Parent: <1A1F7F53.0.1011> + Args:[]etf.Term(nil) +Application started with Pid <1A1F7F53.0.1011>! + +``` diff --git a/examples/application/app.go b/examples/application/app.go new file mode 100644 index 00000000..6895a50e --- /dev/null +++ b/examples/application/app.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +func createDemoApp() gen.ApplicationBehavior { + return &demoApp{} +} + +type demoApp struct { + gen.Application +} + +func (da *demoApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { + return gen.ApplicationSpec{ + Name: "demoApp", + Description: "Demo Applicatoin", + Version: "v.1.0", + Children: []gen.ApplicationChildSpec{ + gen.ApplicationChildSpec{ + Child: createDemoSup(), + Name: "demoSup", + }, + gen.ApplicationChildSpec{ + Child: createDemoServer(), + Name: "demoServer", + }, + }, + }, nil +} + +func (da *demoApp) Start(process gen.Process, args ...etf.Term) { + fmt.Printf("Application started with Pid %s!\n", process.Self()) +} diff --git a/examples/application/demoApplication.go b/examples/application/demoApplication.go deleted file mode 100644 index 8bcc1d08..00000000 --- a/examples/application/demoApplication.go +++ /dev/null @@ -1,146 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "github.com/ergo-services/ergo" - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" - "github.com/ergo-services/ergo/node" -) - -var ( - NodeName string - Cookie string - err error - ListenRangeBegin int - ListenRangeEnd int = 35000 - Listen string - ListenEPMD int - - EnableRPC bool -) - -type demoApp struct { - gen.Application -} - -func (da *demoApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { - return gen.ApplicationSpec{ - Name: "demoApp", - Description: "Demo Applicatoin", - Version: "v.1.0", - Environment: map[gen.EnvKey]interface{}{ - "envName1": 123, - "envName2": "Hello world", - }, - Children: []gen.ApplicationChildSpec{ - gen.ApplicationChildSpec{ - Child: &demoSup{}, - Name: "demoSup", - }, - gen.ApplicationChildSpec{ - Child: &demoGenServ{}, - Name: "justDemoGS", - }, - }, - }, nil -} - -func (da *demoApp) Start(process gen.Process, args ...etf.Term) { - fmt.Println("Application started!") -} - -type demoSup struct { - gen.Supervisor -} - -func (ds *demoSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { - spec := gen.SupervisorSpec{ - Name: "demoAppSup", - Children: []gen.SupervisorChildSpec{ - gen.SupervisorChildSpec{ - Name: "demoServer01", - Child: &demoGenServ{}, - }, - gen.SupervisorChildSpec{ - Name: "demoServer02", - Child: &demoGenServ{}, - Args: []etf.Term{12345}, - }, - gen.SupervisorChildSpec{ - Name: "demoServer03", - Child: &demoGenServ{}, - Args: []etf.Term{"abc", 67890}, - }, - }, - Strategy: gen.SupervisorStrategy{ - Type: gen.SupervisorStrategyOneForAll, - // Type: gen.SupervisorStrategyRestForOne, - // Type: gen.SupervisorStrategyOneForOne, - Intensity: 2, - Period: 5, - Restart: gen.SupervisorStrategyRestartTemporary, - // Restart: gen.SupervisorStrategyRestartTransient, - // Restart: gen.SupervisorStrategyRestartPermanent, - }, - } - return spec, nil -} - -// gen.Server implementation structure -type demoGenServ struct { - gen.Server -} - -func (dgs *demoGenServ) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleCast (%s): %v\n", process.Name(), message) - switch message { - case etf.Atom("stop"): - return gen.ServerStatusStopWithReason("stop they said") - } - return gen.ServerStatusOK -} - -func (dgs *demoGenServ) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - fmt.Printf("HandleCall (%s): %v, From: %v\n", process.Name(), message, from) - - switch message { - case etf.Atom("hello"): - return etf.Atom("hi"), gen.ServerStatusOK - } - - reply := etf.Tuple{etf.Atom("error"), etf.Atom("unknown_request")} - return reply, gen.ServerStatusOK -} - -func init() { - flag.StringVar(&NodeName, "name", "demo@127.0.0.1", "node name") - flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") -} - -func main() { - flag.Parse() - - opts := node.Options{} - - // Initialize new node with given name, cookie, listening port range and epmd port - demoNode, _ := ergo.StartNode(NodeName, Cookie, opts) - - // start application - if _, err := demoNode.ApplicationLoad(&demoApp{}); err != nil { - panic(err) - } - - appProcess, _ := demoNode.ApplicationStart("demoApp") - fmt.Println("Run erl shell:") - fmt.Printf("erl -name %s -setcookie %s\n", "erl-"+demoNode.Name(), Cookie) - - fmt.Println("-----Examples that can be tried from 'erl'-shell") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", "demoServer01", demoNode.Name()) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", "demoServer01", demoNode.Name()) - - appProcess.Wait() - demoNode.Stop() -} diff --git a/examples/application/main.go b/examples/application/main.go new file mode 100644 index 00000000..fd38c831 --- /dev/null +++ b/examples/application/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +func main() { + flag.Parse() + + fmt.Println("") + fmt.Println("to stop press Ctrl-C") + fmt.Println("") + + apps := []gen.ApplicationBehavior{ + createDemoApp(), + } + opts := node.Options{ + Applications: apps, + } + demoNode, err := ergo.StartNode("app@localhost", "cookie", opts) + if err != nil { + panic(err) + } + demoNode.Wait() +} diff --git a/examples/application/server.go b/examples/application/server.go new file mode 100644 index 00000000..22da0f07 --- /dev/null +++ b/examples/application/server.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +func createDemoServer() gen.ServerBehavior { + return &demoServer{} +} + +type demoServer struct { + gen.Server +} + +func (ds *demoServer) Init(process *gen.ServerProcess, args ...etf.Term) error { + fmt.Printf("Started new process\n\tPid: %s\n\tName: %q\n\tParent: %s\n\tArgs:%#v\n", + process.Self(), + process.Name(), + process.Parent().Self(), + args) + return nil +} diff --git a/examples/application/sup.go b/examples/application/sup.go new file mode 100644 index 00000000..ae9f9d70 --- /dev/null +++ b/examples/application/sup.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +func createDemoSup() gen.SupervisorBehavior { + return &demoSup{} +} + +type demoSup struct { + gen.Supervisor +} + +func (ds *demoSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { + spec := gen.SupervisorSpec{ + Name: "demoAppSup", + Children: []gen.SupervisorChildSpec{ + gen.SupervisorChildSpec{ + Name: "demoServer01", + Child: createDemoServer(), + }, + gen.SupervisorChildSpec{ + Name: "demoServer02", + Child: createDemoServer(), + Args: []etf.Term{12345}, + }, + gen.SupervisorChildSpec{ + Name: "demoServer03", + Child: createDemoServer(), + Args: []etf.Term{"abc", 67890}, + }, + }, + Strategy: gen.SupervisorStrategy{ + Type: gen.SupervisorStrategyOneForAll, + Intensity: 2, + Period: 5, + Restart: gen.SupervisorStrategyRestartTemporary, + }, + } + return spec, nil +} diff --git a/examples/erlang/README.md b/examples/erlang/README.md new file mode 100644 index 00000000..ae2f6339 --- /dev/null +++ b/examples/erlang/README.md @@ -0,0 +1,7 @@ +## Erlang demo scenario ## + +This example demonstrates how Ergo's node interop with the Erlang node. + +Here is output of this demonstration +![Screenshot from 2022-07-20 16-06-27](https://user-images.githubusercontent.com/118860/180004548-5916ecdd-f78a-4cae-bca7-3956bd710b0e.png) + diff --git a/examples/erlang/main.go b/examples/erlang/main.go new file mode 100644 index 00000000..9bbbd9d0 --- /dev/null +++ b/examples/erlang/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +var ( + ServerName string + NodeName string + Cookie string +) + +func init() { + flag.StringVar(&ServerName, "server", "example", "server process name") + flag.StringVar(&NodeName, "name", "demo@127.0.0.1", "node name") + flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") +} + +func main() { + flag.Parse() + + fmt.Println("") + fmt.Println("to stop press Ctrl-C") + fmt.Println("") + + node, err := ergo.StartNode(NodeName, Cookie, node.Options{}) + if err != nil { + panic(err) + } + + _, err = node.Spawn(ServerName, gen.ProcessOptions{}, &demo{}) + if err != nil { + panic(err) + } + + fmt.Println("Start erlang node with the command below:") + fmt.Printf(" $ erl -name %s -setcookie %s\n\n", "erl-"+node.Name(), Cookie) + + fmt.Println("----- to make call request from 'erl'-shell:") + fmt.Printf("gen_server:call({%s,'%s'}, hi).\n", ServerName, NodeName) + fmt.Printf("gen_server:call({%s,'%s'}, {echo, 1,2,3}).\n", ServerName, NodeName) + fmt.Println("----- to send cast message from 'erl'-shell:") + fmt.Printf("gen_server:cast({%s,'%s'}, {cast, 1,2,3}).\n", ServerName, NodeName) + fmt.Println("----- send atom 'stop' to stop server :") + fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", ServerName, NodeName) + + node.Wait() +} diff --git a/examples/erlang/server.go b/examples/erlang/server.go new file mode 100644 index 00000000..73260116 --- /dev/null +++ b/examples/erlang/server.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type demo struct { + gen.Server +} + +func (d *demo) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + fmt.Printf("[%s] HandleCast: %#v\n", process.Name(), message) + switch message { + case etf.Atom("stop"): + return gen.ServerStatusStopWithReason("stop they said") + } + return gen.ServerStatusOK +} + +func (d *demo) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { + fmt.Printf("[%s] HandleCall: %#v, From: %s\n", process.Name(), message, from.Pid) + + switch message.(type) { + case etf.Atom: + return "hello", gen.ServerStatusOK + + default: + return message, gen.ServerStatusOK + } +} + +func (d *demo) Terminate(process *gen.ServerProcess, reason string) { + fmt.Printf("[%s] Terminating process with reason %q", process.Name(), reason) +} diff --git a/examples/events/README.md b/examples/events/README.md new file mode 100644 index 00000000..09600cf1 --- /dev/null +++ b/examples/events/README.md @@ -0,0 +1,26 @@ +## Events demo scenario ## + +This example implements simple publisher and subscriber to demonstrate `gen.Event` feature. + +Here is output of this example: + +``` +❯❯❯❯ go run . +Start node eventsnode@localhost +process <91BC521D.0.1011> registered event simple +Started process <91BC521D.0.1011> with name "producer" +Started process <91BC521D.0.1012> with name "consumer" +... producing event: {EVNT 1} +consumer got event: {EVNT 1} +... producing event: {EVNT 2} +consumer got event: {EVNT 2} +... producing event: {EVNT 3} +consumer got event: {EVNT 3} +... producing event: {EVNT 4} +consumer got event: {EVNT 4} +... producing event: {EVNT 5} +consumer got event: {EVNT 5} +producer has terminated +Stop node eventsnode@localhost + +``` diff --git a/examples/events/consumer.go b/examples/events/consumer.go new file mode 100644 index 00000000..eccb27b0 --- /dev/null +++ b/examples/events/consumer.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type consumer struct { + gen.Server +} + +func (c *consumer) Init(process *gen.ServerProcess, args ...etf.Term) error { + if err := process.MonitorEvent(simpleEvent); err != nil { + return err + } + return nil +} + +func (c *consumer) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + switch message.(type) { + case messageSimpleEvent: + fmt.Println("consumer got event: ", message) + case gen.MessageEventDown: + fmt.Println("producer has terminated") + return gen.ServerStatusStop + default: + fmt.Println("unknown message", message) + } + return gen.ServerStatusOK +} diff --git a/examples/events/main.go b/examples/events/main.go new file mode 100644 index 00000000..e882be22 --- /dev/null +++ b/examples/events/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +const ( + simpleEvent gen.Event = "simple" +) + +type messageSimpleEvent struct { + e string +} + +func main() { + flag.Parse() + + fmt.Println("Start node eventsnode@localhost") + myNode, _ := ergo.StartNode("eventsnode@localhost", "cookies", node.Options{}) + + prod, _ := myNode.Spawn("producer", gen.ProcessOptions{}, &producer{}) + fmt.Printf("Started process %s with name %q\n", prod.Self(), prod.Name()) + + cons, _ := myNode.Spawn("consumer", gen.ProcessOptions{}, &consumer{}) + fmt.Printf("Started process %s with name %q\n", cons.Self(), cons.Name()) + + cons.Wait() + fmt.Println("Stop node", myNode.Name()) + myNode.Stop() +} diff --git a/examples/events/producer.go b/examples/events/producer.go new file mode 100644 index 00000000..bf3e1aaf --- /dev/null +++ b/examples/events/producer.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" +) + +type producer struct { + gen.Server +} + +func (p *producer) Init(process *gen.ServerProcess, args ...etf.Term) error { + + if err := process.RegisterEvent(simpleEvent, messageSimpleEvent{}); err != nil { + lib.Warning("can't register event %q: %s", simpleEvent, err) + } + fmt.Printf("process %s registered event %s\n", process.Self(), simpleEvent) + process.SendAfter(process.Self(), 1, time.Second) + return nil +} + +func (p *producer) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + n := message.(int) + if n > 5 { + return gen.ServerStatusStop + } + // sending message with delay 1 second + process.SendAfter(process.Self(), n+1, time.Second) + event := messageSimpleEvent{ + e: fmt.Sprintf("EVNT %d", n), + } + + fmt.Printf("... producing event: %s\n", event) + if err := process.SendEventMessage(simpleEvent, event); err != nil { + fmt.Println("can't send event:", err) + } + return gen.ServerStatusOK +} diff --git a/examples/gendemo/README.md b/examples/gencustom/README.md similarity index 91% rename from examples/gendemo/README.md rename to examples/gencustom/README.md index 36ba3853..433a31ec 100644 --- a/examples/gendemo/README.md +++ b/examples/gencustom/README.md @@ -1,4 +1,4 @@ -## GenDemo ## +## GenCustom ## This example demonstrates how to create a custom behavior (design pattern) on top of the gen.Server. diff --git a/examples/gencustom/gen_custom.go b/examples/gencustom/gen_custom.go new file mode 100644 index 00000000..695a96ac --- /dev/null +++ b/examples/gencustom/gen_custom.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type Custom struct { + gen.Server +} + +type CustomProcess struct { + gen.ServerProcess + counter int +} + +// CustomBehavior interface +type CustomBehavior interface { + // + // Mandatory callbacks + // + + // InitCustom + InitCustom(process *CustomProcess, args ...etf.Term) error + + // HandleHello invoked on a 'hello' + HandleHello(process *CustomProcess) CustomStatus + + // + // Optional callbacks + // + + // HandleCustomCall this callback is invoked on ServerProcess.Call. This method is optional + // for the implementation + HandleCustomCall(process *CustomProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) + // HandleCustomDirect this callback is invoked on Process.Direct. This method is optional + // for the implementation + HandleCustomDirect(process *CustomProcess, message interface{}) (interface{}, error) + // HandleCustomCast this callback is invoked on ServerProcess.Cast. This method is optional + // for the implementation + HandleCustomCast(process *CustomProcess, message etf.Term) gen.ServerStatus + // HandleCustomInfo this callback is invoked on Process.Send. This method is optional + // for the implementation + HandleCustomInfo(process *CustomProcess, message etf.Term) gen.ServerStatus +} + +type CustomStatus error + +var ( + CustomStatusOK CustomStatus = nil + CustomStatusStop CustomStatus = fmt.Errorf("stop") +) + +// default Custom callbacks + +func (gd *Custom) HandleCustomCall(process *CustomProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { + fmt.Printf("HandleCustomCall: unhandled message (from %#v) %#v\n", from, message) + return etf.Atom("ok"), gen.ServerStatusOK +} + +func (gd *Custom) HandleCustomCast(process *CustomProcess, message etf.Term) gen.ServerStatus { + fmt.Printf("HandleCustomCast: unhandled message %#v\n", message) + return gen.ServerStatusOK +} +func (gd *Custom) HandleCustomInfo(process *CustomProcess, message etf.Term) gen.ServerStatus { + fmt.Printf("HandleCustomInfo: unhandled message %#v\n", message) + return gen.ServerStatusOK +} + +// +// API +// + +type messageHello struct{} + +func (d *Custom) Hello(process gen.Process) error { + _, err := process.Direct(messageHello{}) + return err +} + +type messageGetStat struct{} + +func (d *Custom) Stat(process gen.Process) int { + counter, _ := process.Direct(messageGetStat{}) + return counter.(int) +} + +// +// Custom methods. Available to use inside actors only. +// + +func (dp *CustomProcess) Hi() CustomStatus { + dp.counter = dp.counter * 2 + return CustomStatusOK +} + +// +// gen.Server callbacks +// +func (d *Custom) Init(process *gen.ServerProcess, args ...etf.Term) error { + custom := &CustomProcess{ + ServerProcess: *process, + } + // do not inherit parent State + custom.State = nil + + if err := process.Behavior().(CustomBehavior).InitCustom(custom, args...); err != nil { + return err + } + process.State = custom + return nil +} + +func (gd *Custom) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { + custom := process.State.(*CustomProcess) + return process.Behavior().(CustomBehavior).HandleCustomCall(custom, from, message) +} + +func (gd *Custom) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { + custom := process.State.(*CustomProcess) + switch message.(type) { + case messageGetStat: + return custom.counter, gen.DirectStatusOK + case messageHello: + process.Behavior().(CustomBehavior).HandleHello(custom) + custom.counter++ + return nil, gen.DirectStatusOK + default: + return process.Behavior().(CustomBehavior).HandleCustomDirect(custom, message) + } + +} + +func (gd *Custom) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + custom := process.State.(*CustomProcess) + return process.Behavior().(CustomBehavior).HandleCustomCast(custom, message) +} + +func (gd *Custom) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + custom := process.State.(*CustomProcess) + return process.Behavior().(CustomBehavior).HandleCustomInfo(custom, message) +} diff --git a/examples/gendemo/main.go b/examples/gencustom/main.go similarity index 75% rename from examples/gendemo/main.go rename to examples/gencustom/main.go index d388d86e..66948169 100644 --- a/examples/gendemo/main.go +++ b/examples/gencustom/main.go @@ -10,21 +10,21 @@ import ( "github.com/ergo-services/ergo/node" ) -type MyDemo struct { - Demo +type MyCustom struct { + Custom } -func (md *MyDemo) InitDemo(process *DemoProcess, args ...etf.Term) error { - fmt.Printf("Started instance of MyDemo with PID %s and args %v\n", process.Self(), args) +func (md *MyCustom) InitCustom(process *CustomProcess, args ...etf.Term) error { + fmt.Printf("Started instance of MyCustom with PID %s and args %v\n", process.Self(), args) return nil } -func (md *MyDemo) HandleHello(process *DemoProcess) DemoStatus { +func (md *MyCustom) HandleHello(process *CustomProcess) CustomStatus { fmt.Println("got Hello") - return DemoStatusOK + return CustomStatusOK } -func (md *MyDemo) HandleDemoDirect(process *DemoProcess, message interface{}) (interface{}, error) { +func (md *MyCustom) HandleCustomDirect(process *CustomProcess, message interface{}) (interface{}, error) { fmt.Println("Say hi to increase counter twice") process.Hi() @@ -40,7 +40,7 @@ func main() { return } - demo := &MyDemo{} + demo := &MyCustom{} // Spawn a new process with arguments process, e := node.Spawn("demo", gen.ProcessOptions{}, demo, 1, 2, 3) if e != nil { diff --git a/examples/gendemo/gen_demo.go b/examples/gendemo/gen_demo.go deleted file mode 100644 index 8b659d68..00000000 --- a/examples/gendemo/gen_demo.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" -) - -type Demo struct { - gen.Server -} - -type DemoProcess struct { - gen.ServerProcess - counter int -} - -// DemoBehavior interface -type DemoBehavior interface { - // - // Mandatory callbacks - // - - // InitDemo - InitDemo(process *DemoProcess, args ...etf.Term) error - - // HandleHello invoked on a 'hello' - HandleHello(process *DemoProcess) DemoStatus - - // - // Optional callbacks - // - - // HandleDemoCall this callback is invoked on ServerProcess.Call. This method is optional - // for the implementation - HandleDemoCall(process *DemoProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) - // HandleDemoDirect this callback is invoked on Process.Direct. This method is optional - // for the implementation - HandleDemoDirect(process *DemoProcess, message interface{}) (interface{}, error) - // HandleDemoCast this callback is invoked on ServerProcess.Cast. This method is optional - // for the implementation - HandleDemoCast(process *DemoProcess, message etf.Term) gen.ServerStatus - // HandleDemoInfo this callback is invoked on Process.Send. This method is optional - // for the implementation - HandleDemoInfo(process *DemoProcess, message etf.Term) gen.ServerStatus -} - -type DemoStatus error - -var ( - DemoStatusOK DemoStatus = nil - DemoStatusStop DemoStatus = fmt.Errorf("stop") -) - -// default Demo callbacks - -func (gd *Demo) HandleDemoCall(process *DemoProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - fmt.Printf("HandleDemoCall: unhandled message (from %#v) %#v\n", from, message) - return etf.Atom("ok"), gen.ServerStatusOK -} - -func (gd *Demo) HandleDemoCast(process *DemoProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleDemoCast: unhandled message %#v\n", message) - return gen.ServerStatusOK -} -func (gd *Demo) HandleDemoInfo(process *DemoProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleDemoInfo: unhandled message %#v\n", message) - return gen.ServerStatusOK -} - -// -// API -// - -type messageHello struct{} - -func (d *Demo) Hello(process gen.Process) error { - _, err := process.Direct(messageHello{}) - return err -} - -type messageGetStat struct{} - -func (d *Demo) Stat(process gen.Process) int { - counter, _ := process.Direct(messageGetStat{}) - return counter.(int) -} - -// -// Demo methods. Available to use inside actors only. -// - -func (dp *DemoProcess) Hi() DemoStatus { - dp.counter = dp.counter * 2 - return DemoStatusOK -} - -// -// gen.Server callbacks -// -func (d *Demo) Init(process *gen.ServerProcess, args ...etf.Term) error { - demo := &DemoProcess{ - ServerProcess: *process, - } - // do not inherit parent State - demo.State = nil - - if err := process.Behavior().(DemoBehavior).InitDemo(demo, args...); err != nil { - return err - } - process.State = demo - return nil -} - -func (gd *Demo) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - demo := process.State.(*DemoProcess) - return process.Behavior().(DemoBehavior).HandleDemoCall(demo, from, message) -} - -func (gd *Demo) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { - demo := process.State.(*DemoProcess) - switch message.(type) { - case messageGetStat: - return demo.counter, nil - case messageHello: - process.Behavior().(DemoBehavior).HandleHello(demo) - demo.counter++ - return nil, nil - default: - return process.Behavior().(DemoBehavior).HandleDemoDirect(demo, message) - } - -} - -func (gd *Demo) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - demo := process.State.(*DemoProcess) - return process.Behavior().(DemoBehavior).HandleDemoCast(demo, message) -} - -func (gd *Demo) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - demo := process.State.(*DemoProcess) - return process.Behavior().(DemoBehavior).HandleDemoInfo(demo, message) -} diff --git a/examples/raft/README.md b/examples/genraft/README.md similarity index 100% rename from examples/raft/README.md rename to examples/genraft/README.md diff --git a/examples/raft/main.go b/examples/genraft/main.go similarity index 100% rename from examples/raft/main.go rename to examples/genraft/main.go diff --git a/examples/raft/raft.go b/examples/genraft/raft.go similarity index 100% rename from examples/raft/raft.go rename to examples/genraft/raft.go diff --git a/examples/raft/storage.go b/examples/genraft/storage.go similarity index 100% rename from examples/raft/storage.go rename to examples/genraft/storage.go diff --git a/examples/genserver/README.md b/examples/genserver/README.md new file mode 100644 index 00000000..c1deccb5 --- /dev/null +++ b/examples/genserver/README.md @@ -0,0 +1,25 @@ +## Server demo scenario ## + +This example implements simple server which sends a message to itself. + +Here is output of this example: + +``` +❯❯❯❯ go run . +Start node mynode@localhost +Started process <0ABD9258.0.1011> with name "simple" +Send message 100 to itself +HandleInfo: 100 +increase this value by 1 and send it to itself again +HandleInfo: 101 +increase this value by 1 and send it to itself again +HandleInfo: 102 +increase this value by 1 and send it to itself again +HandleInfo: 103 +increase this value by 1 and send it to itself again +HandleInfo: 104 +increase this value by 1 and send it to itself again +HandleInfo: 105 +Stop node mynode@localhost + +``` diff --git a/examples/genserver/main.go b/examples/genserver/main.go new file mode 100644 index 00000000..06187a82 --- /dev/null +++ b/examples/genserver/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +func main() { + flag.Parse() + + fmt.Println("Start node mynode@localhost") + myNode, _ := ergo.StartNode("mynode@localhost", "cookies", node.Options{}) + + process, _ := myNode.Spawn("simple", gen.ProcessOptions{}, &simple{}) + fmt.Printf("Started process %s with name %q\n", process.Self(), process.Name()) + + // send a message to itself + fmt.Println("Send message 100 to itself") + process.Send(process.Self(), 100) + + // wait for the prokcess termination. + process.Wait() + fmt.Println("Stop node", myNode.Name()) + myNode.Stop() +} diff --git a/examples/genserver/server.go b/examples/genserver/server.go index 77af00e7..24a5530e 100644 --- a/examples/genserver/server.go +++ b/examples/genserver/server.go @@ -1,123 +1,26 @@ package main import ( - "flag" "fmt" + "time" - "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" - "github.com/ergo-services/ergo/node" ) -// Server implementation structure -type demo struct { +// simple implementation of Server +type simple struct { gen.Server } -var ( - ServerName string - NodeName string - Cookie string - err error - ListenBegin int - ListenEnd int = 35000 - - EnableRPC bool -) - -func (dgs *demo) Init(process *gen.ServerProcess, args ...etf.Term) error { - fmt.Printf("[%s] Init: args %v \n", process.Name(), args) - return nil -} - -func (dgs *demo) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("[%s] HandleCast: %v\n", process.Name(), message) - if pid, ok := message.(etf.Pid); ok { - process.Send(pid, etf.Atom("hahaha")) - return gen.ServerStatusOK - } - switch message { - case etf.Atom("stop"): - return gen.ServerStatusStopWithReason("stop they said") +func (s *simple) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + value := message.(int) + fmt.Printf("HandleInfo: %#v \n", message) + if value > 104 { + return gen.ServerStatusStop } + // sending message with delay 1 second + fmt.Println("increase this value by 1 and send it to itself again") + process.SendAfter(process.Self(), value+1, time.Second) return gen.ServerStatusOK } - -func (dgs *demo) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - fmt.Printf("[%s] HandleCall: %#v, From: %v\n", process.Name(), message, from) - - reply := etf.Term(etf.Tuple{etf.Atom("error"), etf.Atom("unknown_request")}) - switch message.(type) { - case etf.Atom: - return etf.Term("hi"), gen.ServerStatusOK - - case etf.List: - return message, gen.ServerStatusOK - } - return reply, gen.ServerStatusOK -} - -func init() { - flag.IntVar(&ListenBegin, "listen_begin", 15151, "listen port range") - flag.IntVar(&ListenEnd, "listen_end", 25151, "listen port range") - flag.StringVar(&ServerName, "gen_server_name", "example", "gen_server name") - flag.StringVar(&NodeName, "name", "demo@127.0.0.1", "node name") - flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") -} - -func main() { - flag.Parse() - - opts := node.Options{ - ListenBegin: uint16(ListenBegin), - ListenEnd: uint16(ListenEnd), - } - - // Initialize new node with given name, cookie, listening port range and epmd port - node, e := ergo.StartNode(NodeName, Cookie, opts) - if e != nil { - fmt.Println("error", e) - return - } - - // Initialize new instance of demo structure which implements Process behavior - demoGS := &demo{} - - // Spawn process with one arguments - process, e := node.Spawn(ServerName, gen.ProcessOptions{}, demoGS) - if e != nil { - fmt.Println("error", e) - return - } - - // Add RPC handle with MF "rpc" "request" - fun := func(args ...etf.Term) etf.Term { - if len(args) == 0 { - return etf.Atom("ok") - } - return etf.Term(args) - } - err := node.ProvideRPC("rpc", "request", fun) - if err != nil { - fmt.Println("error", err) - return - } - - // Print how it can be used along with the Erlang node - fmt.Println("Run erl shell:") - fmt.Printf("erl -name %s -setcookie %s\n", "erl-"+node.Name(), Cookie) - - fmt.Println("----- Examples that can be tried from 'erl'-shell") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", ServerName, NodeName) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", ServerName, NodeName) - fmt.Println("----- you may also want to try RPC request two ways: ") - fmt.Println("----- by the registered MF('rpc','request'). will be handled by the registered anonymous func") - fmt.Printf("rpc:call('%s', rpc, request, [hello, 3.14]).\n", NodeName) - fmt.Printf("----- by the process name. will be handled by %s process. function name will be ignored\n", ServerName) - fmt.Printf("rpc:call('%s', %s, f, [hello, 3.14]).\n", NodeName, ServerName) - - process.Wait() - node.Stop() - node.Wait() -} diff --git a/examples/genstage/README.md b/examples/genstage/README.md index 2e211da2..51decf9d 100644 --- a/examples/genstage/README.md +++ b/examples/genstage/README.md @@ -1 +1,37 @@ -This is a producer/consumer example based on gen.Stage feature +## Stage demo scenario ## + +This example demonstrates a simple implementation of publisher/subscriber using gen.Stage behavior. +The basic scenario is sending numbers to the consumers but distinguishing them on odd and even. + +It starts with two nodes - `node_abc@localhost` with one producer and `node_def@localhost` with two consumers. In this example, the producer uses a `partition` dispatcher with the hash function to distinguish numbers (there are three types of dispatchers - `demand`, `broadcast`, `partition`). + +Here is output of this example + +``` +❯❯❯❯ go run . + +to stop press Ctrl-C + +Starting nodes 'node_abc@localhost' and 'node_def@localhost' +Spawn producer on 'node_abc@localhost' +Spawn 2 consumers on 'node_def@localhost' +Subscribe consumer even [ <6E7F6C08.0.1011> ] with min events = 1 and max events 2 +Subscribe consumer odd [ <6E7F6C08.0.1012> ] with min events = 2 and max events 4 +New subscription from: <6E7F6C08.0.1011> with min: 1 and max: 2 +Producer: just got demand for 2 event(s) from <6E7F6C08.0.1011> +Producer. Generate random numbers and send them to consumers... [62 75 36 10 56] +New subscription from: <6E7F6C08.0.1012> with min: 2 and max: 4 +Consumer 'even' got events: [62 36] +Producer: just got demand for 2 event(s) from <6E7F6C08.0.1011> +Producer. Generate random numbers and send them to consumers... [74 64 57 60 6] +Consumer 'even' got events: [60 6] +Consumer 'even' got events: [10 56] +Consumer 'even' got events: [74 64] +Producer: just got demand for 2 event(s) from <6E7F6C08.0.1011> +Producer. Generate random numbers and send them to consumers... [58 62 60 53 63] +Consumer 'even' got events: [60] +Consumer 'even' got events: [58 62] +Producer: just got demand for 2 event(s) from <6E7F6C08.0.1011> +Producer. Generate random numbers and send them to consumers... [47 58 25 93 92] +Consumer 'even' got events: [58 92] +``` diff --git a/examples/genstage/main.go b/examples/genstage/main.go index 298e5e83..2c2533c3 100644 --- a/examples/genstage/main.go +++ b/examples/genstage/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "github.com/ergo-services/ergo" @@ -9,6 +10,12 @@ import ( ) func main() { + flag.Parse() + + fmt.Println("") + fmt.Println("to stop press Ctrl-C") + fmt.Println("") + // create nodes for producer and consumers fmt.Println("Starting nodes 'node_abc@localhost' and 'node_def@localhost'") node_abc, _ := ergo.StartNode("node_abc@localhost", "cookies", node.Options{}) diff --git a/examples/gentcp/README.md b/examples/gentcp/README.md new file mode 100644 index 00000000..8a17fdee --- /dev/null +++ b/examples/gentcp/README.md @@ -0,0 +1,24 @@ +## TCP demo scenario ## + +This example implements a simple application that starts the child process with TCP server + +Here is output of this example: +``` +❯❯❯❯ go run . -tls +Start node tcp@127.0.0.1 +TLS enabled. Generated self signed certificate. You may check it with command below: + $ openssl s_client -connect :8383 +Application started! +[TCP handler] got new connection from "127.0.0.1:35962" +send string "d1f5d8f197f0a4c8" to "127.0.0.1:8383" +[TCP handler] got message from "127.0.0.1:35962": "d1f5d8f197f0a4c8" +send string "4abff0c11d36ed56" to "127.0.0.1:8383" +[TCP handler] got message from "127.0.0.1:35962": "4abff0c11d36ed56" +send string "d2c5e85c04c0b946" to "127.0.0.1:8383" +[TCP handler] got message from "127.0.0.1:35962": "d2c5e85c04c0b946" +send string "782559e4d6ec170c" to "127.0.0.1:8383" +[TCP handler] got message from "127.0.0.1:35962": "782559e4d6ec170c" +send string "08d2eced5329143c" to "127.0.0.1:8383" +[TCP handler] got message from "127.0.0.1:35962": "08d2eced5329143c" +stop node tcp@127.0.0.1 +``` diff --git a/examples/http/app.go b/examples/gentcp/app.go similarity index 51% rename from examples/http/app.go rename to examples/gentcp/app.go index 94728017..c7a6b154 100644 --- a/examples/http/app.go +++ b/examples/gentcp/app.go @@ -7,28 +7,24 @@ import ( "github.com/ergo-services/ergo/gen" ) -type App struct { +type tcpApp struct { gen.Application } -var ( - handler_sup = &HandlerSup{} -) - -func (a *App) Load(args ...etf.Term) (gen.ApplicationSpec, error) { +func (ta *tcpApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { return gen.ApplicationSpec{ - Name: "WebApp", - Description: "Demo Web Application", + Name: "tcpApp", + Description: "Demo TCP Applicatoin", Version: "v.1.0", Children: []gen.ApplicationChildSpec{ gen.ApplicationChildSpec{ - Child: handler_sup, - Name: "handler_sup", + Child: &tcpServer{}, // tcp_server.go + Name: "tcp", }, }, }, nil } -func (a *App) Start(process gen.Process, args ...etf.Term) { +func (ta *tcpApp) Start(process gen.Process, args ...etf.Term) { fmt.Println("Application started!") } diff --git a/examples/gentcp/main.go b/examples/gentcp/main.go new file mode 100644 index 00000000..7751dde7 --- /dev/null +++ b/examples/gentcp/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "net" + "strconv" + "time" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +var ( + TCPListenPort int + TCPListenHost string + TCPEnableTLS bool +) + +func init() { + flag.IntVar(&TCPListenPort, "port", 8383, "listen port number") + flag.StringVar(&TCPListenHost, "host", "", "listen on host") + flag.BoolVar(&TCPEnableTLS, "tls", false, "enable TLS") +} + +func main() { + var connection net.Conn + var err error + + flag.Parse() + opts := node.Options{ + Applications: []gen.ApplicationBehavior{ + &tcpApp{}, // app.go + }, + } + + fmt.Println("Start node", "tcp@127.0.0.1") + tcpNode, err := ergo.StartNode("tcp@127.0.0.1", "secret", opts) + if err != nil { + panic(err) + } + + hostPort := net.JoinHostPort(TCPListenHost, strconv.Itoa(TCPListenPort)) + dialer := net.Dialer{} + + if TCPEnableTLS { + tlsdialer := tls.Dialer{ + NetDialer: &dialer, + Config: &tls.Config{ + InsecureSkipVerify: true, + }, + } + connection, err = tlsdialer.Dial("tcp", hostPort) + } else { + connection, err = dialer.Dial("tcp", hostPort) + } + + if err != nil { + return + } + + defer connection.Close() + + for i := 0; i < 5; i++ { + str := lib.RandomString(16) + + fmt.Printf("send string %q to %q\n", str, connection.RemoteAddr().String()) + connection.Write([]byte(str)) + time.Sleep(time.Second) + } + + fmt.Println("stop node", tcpNode.Name()) + tcpNode.Stop() +} diff --git a/examples/gentcp/tcp_handler.go b/examples/gentcp/tcp_handler.go new file mode 100644 index 00000000..936f3cdf --- /dev/null +++ b/examples/gentcp/tcp_handler.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/gen" +) + +type tcpHandler struct { + gen.TCPHandler +} + +func (th *tcpHandler) HandleConnect(process *gen.TCPHandlerProcess, conn *gen.TCPConnection) gen.TCPHandlerStatus { + fmt.Printf("[TCP handler] got new connection from %q\n", conn.Addr.String()) + return gen.TCPHandlerStatusOK +} +func (th *tcpHandler) HandleDisconnect(process *gen.TCPHandlerProcess, conn *gen.TCPConnection) { + fmt.Printf("[TCP handler] connection with %q terminated\n", conn.Addr.String()) +} + +func (th *tcpHandler) HandlePacket(process *gen.TCPHandlerProcess, packet []byte, conn *gen.TCPConnection) (int, int, gen.TCPHandlerStatus) { + fmt.Printf("[TCP handler] got message from %q: %q\n", conn.Addr.String(), string(packet)) + + // If you want to send a reply message, use conn.Socket.Write(reply) for that. + + // You may keep any data related to this connection in conn.State + + // return values: left, await, status + // left - how many bytes are left in the packet buffer (you might have + // received a part of the next logical data). + // await - what exact number of bytes you expect in the next packet + // or leave it 0 if you are unsure. + // status - return gen.TCPHandlerStatusClose to close this connection + + // example: + // expected data of 5 bytes, but have received packet = []byte{1,2,3,4,5,6,7,8} + // you must return 3, 5, gen.TCPHandlerStatusOK + // So the following invocation will happen after receiving two more bytes + // The next packet will have []byte{6,7,8,9,0} + return 0, 0, gen.TCPHandlerStatusOK +} diff --git a/examples/gentcp/tcp_server.go b/examples/gentcp/tcp_server.go new file mode 100644 index 00000000..5f9d14cb --- /dev/null +++ b/examples/gentcp/tcp_server.go @@ -0,0 +1,34 @@ +package main + +import ( + "crypto/tls" + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" +) + +type tcpServer struct { + gen.TCP +} + +func (ts *tcpServer) InitTCP(process *gen.TCPProcess, args ...etf.Term) (gen.TCPOptions, error) { + options := gen.TCPOptions{ + Host: TCPListenHost, + Port: uint16(TCPListenPort), + Handler: &tcpHandler{}, + } + + if TCPEnableTLS { + cert, _ := lib.GenerateSelfSignedCert("localhost") + fmt.Println("TLS enabled. Generated self signed certificate. You may check it with command below:") + fmt.Printf(" $ openssl s_client -connect %s:%d\n", TCPListenHost, TCPListenPort) + options.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + } + } + + return options, nil +} diff --git a/examples/genudp/README.md b/examples/genudp/README.md new file mode 100644 index 00000000..9573b72c --- /dev/null +++ b/examples/genudp/README.md @@ -0,0 +1,21 @@ +## UDP demo scenario ## + +This example implements a simple application that starts the child process with UDP server + +Here is output of this example: +``` +❯❯❯❯ go run . +Start node udp@127.0.0.1 +Application started! +send string "b65574c95fbdcd8e" to "[::1]:5533" +[UDP handler] got message from "[::1]:49634": "b65574c95fbdcd8e" +send string "64c203bbf65e121c" to "[::1]:5533" +[UDP handler] got message from "[::1]:49634": "64c203bbf65e121c" +send string "2f2d280a8ad76bd4" to "[::1]:5533" +[UDP handler] got message from "[::1]:49634": "2f2d280a8ad76bd4" +send string "7c125d26809ec335" to "[::1]:5533" +[UDP handler] got message from "[::1]:49634": "7c125d26809ec335" +send string "01317dae8aa231b2" to "[::1]:5533" +[UDP handler] got message from "[::1]:49634": "01317dae8aa231b2" +stop node udp@127.0.0.1 +``` diff --git a/examples/genudp/app.go b/examples/genudp/app.go new file mode 100644 index 00000000..827e66c3 --- /dev/null +++ b/examples/genudp/app.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type udpApp struct { + gen.Application +} + +func (ua *udpApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { + return gen.ApplicationSpec{ + Name: "udpApp", + Description: "Demo UDP Applicatoin", + Version: "v.1.0", + Children: []gen.ApplicationChildSpec{ + gen.ApplicationChildSpec{ + Child: &udpServer{}, // udp_server.go + Name: "udp", + }, + }, + }, nil +} + +func (ua *udpApp) Start(process gen.Process, args ...etf.Term) { + fmt.Println("Application started!") +} diff --git a/examples/genudp/main.go b/examples/genudp/main.go new file mode 100644 index 00000000..f4023dfb --- /dev/null +++ b/examples/genudp/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "net" + "strconv" + "time" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" + "github.com/ergo-services/ergo/node" +) + +var ( + UDPListenPort int + UDPListenHost string +) + +func init() { + flag.IntVar(&UDPListenPort, "port", 5533, "listen port number") + flag.StringVar(&UDPListenHost, "host", "localhost", "listen on host") +} + +func main() { + flag.Parse() + + opts := node.Options{ + Applications: []gen.ApplicationBehavior{ + &udpApp{}, // app.go + }, + } + + fmt.Println("Start node", "udp@127.0.0.1") + udpNode, err := ergo.StartNode("udp@127.0.0.1", "secret", opts) + if err != nil { + panic(err) + } + + hostPort := net.JoinHostPort(UDPListenHost, strconv.Itoa(UDPListenPort)) + c, err := net.Dial("udp", hostPort) + if err != nil { + return + } + defer c.Close() + for i := 0; i < 5; i++ { + str := lib.RandomString(16) + + fmt.Printf("send string %q to %q\n", str, c.RemoteAddr().String()) + c.Write([]byte(str)) + time.Sleep(time.Second) + } + + fmt.Println("stop node", udpNode.Name()) + udpNode.Stop() +} diff --git a/examples/genudp/udp_handler.go b/examples/genudp/udp_handler.go new file mode 100644 index 00000000..a9b228c4 --- /dev/null +++ b/examples/genudp/udp_handler.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/gen" +) + +type udpHandler struct { + gen.UDPHandler +} + +func (uh *udpHandler) HandlePacket(process *gen.UDPHandlerProcess, data []byte, packet gen.UDPPacket) { + fmt.Printf("[UDP handler] got message from %q: %q\n", packet.Addr.String(), string(data)) + + // If you want to send a reply message, use packet.Socket.Write(reply) for that. +} diff --git a/examples/genudp/udp_server.go b/examples/genudp/udp_server.go new file mode 100644 index 00000000..040ada26 --- /dev/null +++ b/examples/genudp/udp_server.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type udpServer struct { + gen.UDP +} + +func (us *udpServer) InitUDP(process *gen.UDPProcess, args ...etf.Term) (gen.UDPOptions, error) { + return gen.UDPOptions{ + Host: UDPListenHost, + Port: uint16(UDPListenPort), + Handler: &udpHandler{}, // udp_handler.go + }, nil +} diff --git a/examples/genweb/README.md b/examples/genweb/README.md new file mode 100644 index 00000000..a79125b1 --- /dev/null +++ b/examples/genweb/README.md @@ -0,0 +1,39 @@ +## Web demo scenario ## + +This example implements a simple application that starts two child processes - webServer and timeServer. + +``` +webNode (node.Node) main.go -> webApp (gen.Application) app.go + | + -> webServer (gen.Web) web.go + | | + | ->> url '/' handler (gen.WebHandler) web_root_handler.go + | ->> url '/time/' handler (gen.WebHandler) web_time_handler.go + | + -> timeServer (gen.Server) time.go +``` + +`webServer` implements gen.Web behavior and defines options for the HTTP server and HTTP handlers with two endpoints: + * `/` - simple response with "Hello" + * `/time/` - demonstrates async handling HTTP requests using `timeServer` + +By default, it starts HTTP server on port 8080 so you can check it using your web-browser [http://localhost:8080/](http://localhost:8080/) or [http://localhost:8080/time/](http://localhost:8080/time/). + +You may also want to benchmark this example on your hardware using the popular tool [wrk](https://github.com/wg/wrk). Here is the result of benchmarking on the AMD Ryzen Threadripper 3970X (64) @ 3.700GHz: + +``` +❯❯❯❯ wrk -t32 -c5000 --latency http://localhost:8080/ +Running 10s test @ http://localhost:8080/ + 32 threads and 5000 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 14.44ms 20.62ms 389.13ms 88.66% + Req/Sec 19.29k 2.85k 69.93k 84.94% + Latency Distribution + 50% 6.93ms + 75% 19.59ms + 90% 37.81ms + 99% 98.17ms + 6149762 requests in 10.10s, 709.65MB read +Requests/sec: 608973.73 +Transfer/sec: 70.27MB +``` diff --git a/examples/genweb/app.go b/examples/genweb/app.go new file mode 100644 index 00000000..a82a1ba2 --- /dev/null +++ b/examples/genweb/app.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type webApp struct { + gen.Application +} + +func (wa *webApp) Load(args ...etf.Term) (gen.ApplicationSpec, error) { + return gen.ApplicationSpec{ + Name: "webApp", + Description: "Demo Web Applicatoin", + Version: "v.1.0", + Children: []gen.ApplicationChildSpec{ + gen.ApplicationChildSpec{ + Child: &webServer{}, // web.go + Name: "web", + }, + gen.ApplicationChildSpec{ + Child: &timeServer{}, // time.go + Name: "time", + }, + }, + }, nil +} + +func (wa *webApp) Start(process gen.Process, args ...etf.Term) { + fmt.Println("Application started!") +} diff --git a/examples/genweb/main.go b/examples/genweb/main.go new file mode 100644 index 00000000..e758177b --- /dev/null +++ b/examples/genweb/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +var ( + WebListenPort int + WebListenHost string + WebEnableTLS bool +) + +func init() { + flag.IntVar(&WebListenPort, "port", 8080, "listen port number. Default port 8080, for TLS 8443") + flag.StringVar(&WebListenHost, "host", "localhost", "listen on host") + flag.BoolVar(&WebEnableTLS, "tls", false, "enable TLS") +} + +func main() { + fmt.Println("") + fmt.Println("to stop press Ctrl-C") + fmt.Println("") + + flag.Parse() + + opts := node.Options{ + Applications: []gen.ApplicationBehavior{ + &webApp{}, // app.go + }, + } + + webNode, err := ergo.StartNode("web@127.0.0.1", "secret", opts) + if err != nil { + panic(err) + } + + webNode.Wait() +} diff --git a/examples/genweb/time.go b/examples/genweb/time.go new file mode 100644 index 00000000..8b7a32c5 --- /dev/null +++ b/examples/genweb/time.go @@ -0,0 +1,37 @@ +package main + +import ( + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" +) + +type timeServer struct { + gen.Server +} + +type messageTimeServerRequest struct { + from etf.Pid + ref etf.Ref +} + +type messageTimeServerReply struct { + ref etf.Ref + time time.Time +} + +func (ts *timeServer) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + switch m := message.(type) { + case messageTimeServerRequest: + reply := messageTimeServerReply{ + ref: m.ref, + time: time.Now(), + } + process.Cast(m.from, reply) + default: + lib.Warning("got unknown message %#v", m) + } + return gen.ServerStatusOK +} diff --git a/examples/genweb/web.go b/examples/genweb/web.go new file mode 100644 index 00000000..3e7d52d6 --- /dev/null +++ b/examples/genweb/web.go @@ -0,0 +1,49 @@ +package main + +import ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" +) + +type webServer struct { + gen.Web +} + +func (w *webServer) InitWeb(process *gen.WebProcess, args ...etf.Term) (gen.WebOptions, error) { + var options gen.WebOptions + + options.Port = uint16(WebListenPort) + options.Host = WebListenHost + proto := "http" + if WebEnableTLS { + cert, err := lib.GenerateSelfSignedCert("gen.Web demo") + if err != nil { + return options, err + } + options.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + proto = "https" + } + + mux := http.NewServeMux() + whOptions := gen.WebHandlerOptions{ + NumHandlers: 50, + IdleTimeout: 10, + RequestTimeout: 20, + } + webRoot := process.StartWebHandler(&rootHandler{}, whOptions) + webTime := process.StartWebHandler(&timeHandler{}, gen.WebHandlerOptions{}) + mux.Handle("/", webRoot) + mux.Handle("/time/", webTime) + options.Handler = mux + + fmt.Printf("Start Web server on %s://%s:%d/\n", proto, WebListenHost, WebListenPort) + + return options, nil +} diff --git a/examples/genweb/web_root_handler.go b/examples/genweb/web_root_handler.go new file mode 100644 index 00000000..8734cd40 --- /dev/null +++ b/examples/genweb/web_root_handler.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/ergo-services/ergo/gen" +) + +type rootHandler struct { + gen.WebHandler +} + +func (r *rootHandler) HandleRequest(process *gen.WebHandlerProcess, request gen.WebMessageRequest) gen.WebHandlerStatus { + request.Response.Write([]byte("Hello")) + return gen.WebHandlerStatusDone +} diff --git a/examples/genweb/web_time_handler.go b/examples/genweb/web_time_handler.go new file mode 100644 index 00000000..ecd70e45 --- /dev/null +++ b/examples/genweb/web_time_handler.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +type timeHandler struct { + gen.WebHandler +} + +type processState struct { + asyncRequests map[etf.Ref]gen.WebMessageRequest +} + +func (th *timeHandler) HandleRequest(process *gen.WebHandlerProcess, request gen.WebMessageRequest) gen.WebHandlerStatus { + state, ok := process.State.(*processState) + if !ok { + state = &processState{ + asyncRequests: make(map[etf.Ref]gen.WebMessageRequest), + } + process.State = state + } + mt := messageTimeServerRequest{ + ref: request.Ref, + from: process.Self(), + } + if err := process.Cast("time", mt); err == nil { + state.asyncRequests[mt.ref] = request + return gen.WebHandlerStatusWait + } + + request.Response.WriteHeader(http.StatusServiceUnavailable) // 503 + return gen.WebHandlerStatusDone +} + +func (th *timeHandler) HandleWebHandlerCast(process *gen.WebHandlerProcess, message etf.Term) gen.ServerStatus { + state, ok := process.State.(*processState) + if !ok { + return gen.ServerStatusOK + } + + switch m := message.(type) { + case messageTimeServerReply: + request, ok := state.asyncRequests[m.ref] + if !ok { + return gen.ServerStatusOK + } + delete(state.asyncRequests, m.ref) + timeResult := fmt.Sprintf("time: %s", m.time) + request.Response.Write([]byte(timeResult)) + process.Reply(m.ref, nil, nil) + + } + return gen.ServerStatusOK +} diff --git a/examples/http/README.md b/examples/http/README.md deleted file mode 100644 index ec7d7427..00000000 --- a/examples/http/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# HTTP and Ergo framework - -This is a simple application to demonstrate how you can combine stateless (HTTP) and stateful (actors) using Ergo Framework. - -* app.go - declares Ergo application and controls handler_sup supervisor -* handler_sup.go - declares SimpleOneForOne supervisor (dynamic pool of workers). Every HTTP-request calls supervisor to start a new child process to handle this request -* handler.go - declares GenServer process to handle HTTP-requests. - -You may also want to use another way to create a worker pool with a fixed number of worker-processes (use OneForOne for that) diff --git a/examples/http/handler.go b/examples/http/handler.go deleted file mode 100644 index 6c9fda35..00000000 --- a/examples/http/handler.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" -) - -// GenServer implementation structure -type Handler struct { - gen.Server -} - -type st struct { - r *http.Request -} - -type response struct { - Request string - Answer string -} - -// Init initializes process state using arbitrary arguments -// Init(...) -> state -func (h *Handler) Init(process *gen.ServerProcess, args ...etf.Term) error { - fmt.Println("Start handling http request") - process.State = &st{ - r: args[0].(*http.Request), - } - return nil -} - -func (h *Handler) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Println(process.State.(*st).r.URL.Path) - w := message.(http.ResponseWriter) - w.Header().Set("Content-Type", "application/json") - response := response{ - Request: process.State.(*st).r.URL.Path, - Answer: "Your request has been handled by " + process.Self().String(), - } - - err := json.NewEncoder(w).Encode(response) - if err != nil { - fmt.Println(err) - } - fmt.Println("Finish handling http request and stop the server", process.Self()) - return gen.ServerStatusStop -} diff --git a/examples/http/handler_sup.go b/examples/http/handler_sup.go deleted file mode 100644 index 34837bfa..00000000 --- a/examples/http/handler_sup.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" -) - -type HandlerSup struct { - gen.Supervisor -} - -func (hs *HandlerSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { - return gen.SupervisorSpec{ - Name: "handler_sup", - Children: []gen.SupervisorChildSpec{ - gen.SupervisorChildSpec{ - Name: "handler", - Child: &Handler{}, - }, - }, - Strategy: gen.SupervisorStrategy{ - Type: gen.SupervisorStrategySimpleOneForOne, - Intensity: 5, - Period: 5, - Restart: gen.SupervisorStrategyRestartTemporary, - }, - }, nil -} diff --git a/examples/http/main.go b/examples/http/main.go deleted file mode 100644 index dba1cca5..00000000 --- a/examples/http/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "net/http" - - "github.com/ergo-services/ergo" - "github.com/ergo-services/ergo/node" -) - -var ( - NodeName string - Cookie string - ListenRangeBegin int - ListenRangeEnd int = 35000 - Listen string - ListenEPMD int -) - -func init() { - flag.StringVar(&NodeName, "name", "web@127.0.0.1", "node name") - flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") -} - -func main() { - flag.Parse() - - opts := node.Options{} - - // Initialize new node with given name, cookie, listening port range and epmd port - nodeHTTP, _ := ergo.StartNode(NodeName, Cookie, opts) - - // start application - if _, err := nodeHTTP.ApplicationLoad(&App{}); err != nil { - panic(err) - } - - process, _ := nodeHTTP.ApplicationStart("WebApp") - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - p := process.ProcessByName("handler_sup") - if p == nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - if handlerProcess, err := handler_sup.StartChild(p, "handler", r); err == nil { - process.Send(handlerProcess.Self(), w) - handlerProcess.Wait() - return - } - - w.WriteHeader(http.StatusInternalServerError) - }) - - go http.ListenAndServe(":8080", nil) - fmt.Println("HTTP is listening on http://127.0.0.1:8080") - - process.Wait() - nodeHTTP.Stop() -} diff --git a/examples/nodetls/example.crt b/examples/nodetls/example.crt deleted file mode 100644 index d4e746a5..00000000 --- a/examples/nodetls/example.crt +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIUMJyHenma6WhhW3cB9mk3JxFCEHIwDQYJKoZIhvcNAQEL -BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMjMyMjE3MTBaFw0zMDEx -MjEyMjE3MTBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw -HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQDLJLqBWnM1t4CLOM+UD3/7y6FEJ7C8O7vUypBUtGTz -5NI4cmBRM4wOxR5Q79Cv9kPxWnDp/atKb5djElj/A2cryRRkHnLfkN19GwcoH1Y1 -otrWd3tu2qsjUZruTeTe3TXOZvF+uDfXGVwGzOdmIAPY94+aBflN2Tp63pCGY/Dl -ieJlEEdGAO81uggptsWKbxULWdMmJmGzAi85baygwIPMI6xaCkUEqmom+pqgEmN1 -tI6qSG23ueqqyuRirfM0wp/6TQm1fEWR67bmDuheSpWCXw/tRNso1ADgfgBaDmdr -s54CnsVmZMwyjGIw4Vrj1jcd0J7knAWdcZscp3ZlzUHHC7b16ufvrhQllbU9dEEB -HF+9hVCDT4OIUxESn2e28bbryc1OrbwmbhZM92Ica2UsB+FF+rV+WS2DZvjG+IWV -x4nccK/eo/FGjXtCtTucYO/bjCFZmhXrnOl+na8KcE5pb8EffrCOAmAdV9RflTIu -yOu/vVwI4gqcCFEkMRjqtFESmiARGCiGO6CbYDoNzwFIo/8Fs88VGhEo+xIMi7GU -9XUliHI2IDOw/0a8U07CZZHQZAukkgSObci6u6iG0V8NCSg7dTB9obs1qp5tUi6o -07aX+0tBquRNMLnTVh0hv7NifBTID5YlVVojdvN/ZVkyzGN5Tk1yQH/vcGpS6sn9 -TwIDAQABo1MwUTAdBgNVHQ4EFgQU4ZyiC0tj9fLtWwRfkIAJz6wIsoUwHwYDVR0j -BBgwFoAU4ZyiC0tj9fLtWwRfkIAJz6wIsoUwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAgEAB1Vf/qwGO9oW/m6JGRw6IMYxGCVAcRpFWi6upLIyvQus -32F2FLxh9xE9mzFjPxbgWxE/I4mZvC2wnE9DtpdG1n+O6FID9d5KyfyhSv29mFZ7 -w/Gef1KOoO/O7L+IXBIfg8If2gFPgJh54alNK0rxJWXNwYfg8VgrS7g8MEROkYV7 -Wl8phSRGwrESgBm3c2GM+TcpRFmIXHMDy9UP+Y6RdwUB5stoT4kqO5XSqi92IgKS -sTcEF28PlHsCAfUtZ3J7K6KNOuEODK9ns+TTgcyd0ZoiroQZhsHqRjqcY5Jwwdz7 -kGK1krZerYvWI4+8qH2+XUe4jtG0wDBXGZRLTXStCZ0g5eLHZQJTvbOgokQhD1HF -hMkKmqu8lcjZwnwRKMv899bFguQeAsdeMyowXb2U2ohx+XpsdAVgzQ7QFKLGFC9a -dgzMU65CW+5bZoYPOT2e08L5eFhkE/RGDicMcxOJXXY4it/mowqmUO+UygvYFExJ -MTIaYb49vqemkkvYG8bMXAwBkjxUIetOckRYp/uyANdpnXj2kzDYuaA5B8saYWi5 -xV4qjj3kTORF1KbVWCwV1BxYfqYlhwo3yCBvMwfvlOQPOwrnVd8K6j5W4kKBvgkd -IrFeWOsDxvDWj+OuGVb7b0iLIhAmkUeSO1622RMKvUZT4+eBUrsn2Exn9zeSs40= ------END CERTIFICATE----- diff --git a/examples/nodetls/example.key b/examples/nodetls/example.key deleted file mode 100644 index c26382d3..00000000 --- a/examples/nodetls/example.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDLJLqBWnM1t4CL -OM+UD3/7y6FEJ7C8O7vUypBUtGTz5NI4cmBRM4wOxR5Q79Cv9kPxWnDp/atKb5dj -Elj/A2cryRRkHnLfkN19GwcoH1Y1otrWd3tu2qsjUZruTeTe3TXOZvF+uDfXGVwG -zOdmIAPY94+aBflN2Tp63pCGY/DlieJlEEdGAO81uggptsWKbxULWdMmJmGzAi85 -baygwIPMI6xaCkUEqmom+pqgEmN1tI6qSG23ueqqyuRirfM0wp/6TQm1fEWR67bm -DuheSpWCXw/tRNso1ADgfgBaDmdrs54CnsVmZMwyjGIw4Vrj1jcd0J7knAWdcZsc -p3ZlzUHHC7b16ufvrhQllbU9dEEBHF+9hVCDT4OIUxESn2e28bbryc1OrbwmbhZM -92Ica2UsB+FF+rV+WS2DZvjG+IWVx4nccK/eo/FGjXtCtTucYO/bjCFZmhXrnOl+ -na8KcE5pb8EffrCOAmAdV9RflTIuyOu/vVwI4gqcCFEkMRjqtFESmiARGCiGO6Cb -YDoNzwFIo/8Fs88VGhEo+xIMi7GU9XUliHI2IDOw/0a8U07CZZHQZAukkgSObci6 -u6iG0V8NCSg7dTB9obs1qp5tUi6o07aX+0tBquRNMLnTVh0hv7NifBTID5YlVVoj -dvN/ZVkyzGN5Tk1yQH/vcGpS6sn9TwIDAQABAoICADhAv3S6e1TQr8Pdw32YnTQ4 -uzuIUiSN1gGi5jzOh3YSUzRWV92kjJA6fZ6kCgHwC/h1tvbUy+4c4KsKlaJoatVx -JThkRiMqlmriZSTzKIhJxJfHFmMoImPxYRnEcDBWyWOSliUlFjF2UEmBzEI3c1lN -lHJuXQ71rIABybutSTQG7q5Vx6bW82bJUSFb/2/KOuWdxh62Wi+b1z/r4vXQ2a5Z -4ow8c8hK+II6uz7AWNJrYWY+EEPkM9t/u6anzMU7b9l8I8gh1ZIG1+r1Dduug6BK -erqVaqrvqh6ARdCqVHE6l/LZzIgCOZl1zmsCvIyC8VhMQPPFULi8kNtqdBrUr/XU -GSmeDa6YZZqEExuiXsAjSdjoqhMT1b2MjRFAmVroVndCKc4pLkXrClTgPeI+g5JO -1Bn/Ue8AvPF6yLmRTEa43G7ECRJ+pXDIcJ+NdlwQy1KjHmK3zWwKWMxPX3Jq4wap -Z8W45f+RYJ6GEIci7tVU5KDaZodW1MLuJmS91kMnJO/UrubhozAM4OcPOwS7PPex -75cRLjK/1L2d4hu+d7hxkKLnWxxKLIBU4saqnzQhaOjAQjokSop4F3nlIn/59A0e -TAlhq3fzCDimwFOqF6tptnFkZTC/uKfMc7yqJw15RUyyTs0ba2QqJuU7poSAxSmA -GeSMudWi178EaBc3yGhBAoIBAQDuPGpM9iWtT0sdizFCExOoU5pA4ecuvj8dFYqa -GAhsndzeouKTq4G0z2o/WxuGYw1oW1S5VQoUfLjIOAguXo+8ct8j6b89RKDp/Bp0 -MCy/Xxs2UignrwfPtI4uzLhfcvHR+dn6Gsh2AJjE+i/b7RJrd5FGck7tC3RhZkQR -+luSOrC00/3rT5eMcCZjcl2Pz8RKCgfSKrEmCNuHUR0QaX2vpI5J3XYi+dQOe+tr -XkpMxKadfcI6GtfVyqaBUvnoOtxvGYbFcpUDuMFLmI3mS6eq9UKWavhYcHjnNU5j -lwVR3ByXWIl/VCxbI7biUbj3GSCG/6I1E2ipBxC2RS/VuHAnAoIBAQDaSnJ2ZoXs -2SAS+K4O78SvjDg40qerDoGMDNQO3eEqZ1NL+dX4JcJUDAkboogyLAgh1LMyMw5F -T+Dpba7K+Ql4HV7okMPwsNSdNrUsl4SFMTtdEuCPHsfuUQBXsmP53znTLbH1Cz3p -W9oFA2qGr3fVcSqVutAOcWP8U4DCmLSF5uMWAoC+rTxS+jvFSPVkWn8ejTZLpZOM -GtiPH6ZnM64w+pB5PA4QLHR6e1HROulG1Frnvxv5ZDq0LXKss7U/hw84tUbcBf81 -VsIGwrFmpgtaRSemWVu0KSEfUX7TqdVUPcT30GV3JD4TgRVBWitw/dpWCz5untXl -mein4D9aXRqZAoIBAEolWXw8e7t1204FnT4QS+TuqCqbZGVC5se5hZqx8iVD+JL+ -JQCKt2K7zziKtYVc3LZm/nZ0BiAcNTJzZfBwk9G1+sbloBSEgIRyZxVUTQj+o6yD -Y2X+brLxYfMk2hky4BpW5cWWgl9fjix4JV8QaNQzsW44c2IJV55cwsDJp6haRdbx -x6xt6B/YY8o9tOrmYhQdDRQXH21UAmcaEq6h9nEtCO/qUjNOh/Y+ESYogX1lEuof -Uszcv+IVIT9MwOTLNQIK8swO/lvbK6XIhBlx4Gwvyxqfjk2QK8Dh8VTDku9IT0m3 -T4vmeauf8PJ9NtEI6/u4IhbcxI0e2s+vttSQg68CggEAdfoeetdWMnf22coFaJU1 -nBsQl8ViURT59xNH2PEaLKzDXCCfAAqISJxon8LsERGzI3Wtk8f8QoG1cPOSsKh/ -8acOEGuNOpyXjJBwwrTxLns5NkhpjXB5Zdfpc3w6hGWc/wGHWITG5UR7RJJgFILp -JTaQaXQZ1nR6MXl/8axOhMAQo2ie5G8EV2RewXV5Cs/OPFjdq2zFncc0m8XjTYuk -7Vu+kYdfomYkXb4grhBE278Rkoa7O8Jr581YWPaXUspP87olneyvzcgh/T1kW7IK -GLOfhkxtP6Wq/R4yiXsUjP8FYVoEPKwd0LTHJBbzu5G3WyGgkHDP8dOI5pJQKyp+ -oQKCAQEAqF7vSRml4BOPKY270p6byTmpUL2WKm4kM86jJFMGyVM2SP5ikMC/BEiB -I+dVrZwISjtsGpWkf4GqL38O+g8ASsfr42VYhq6Er2/9PuKQVYqRiu5nJGjE4Yv0 -tvaa3Kdji9PWP73KyFr4kObh4h+RWpINnUiZMfeDnpjB/pJFk8BniRxjhRmBAKMD -hvbNqcKEgwyMvMjmooYx6A5HHeysW4TPaYBs5tiMItqYYotc70rKJrPw8TGOBsHw -iJzrhgeoN87eZXeT8+UD7vVXXq+lq1C90NkINiK10z6iUjqnt9xIxPFUkQGWD1Uz -I3tVJgeRtsriQrwiroE6sFfQjCXhdg== ------END PRIVATE KEY----- diff --git a/examples/nodetls/tlsGenServer.go b/examples/nodetls/tlsGenServer.go deleted file mode 100644 index b5dd8d27..00000000 --- a/examples/nodetls/tlsGenServer.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "github.com/ergo-services/ergo" - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" - "github.com/ergo-services/ergo/node" -) - -// GenServer implementation structure -type demoGenServ struct { - gen.Server -} - -var ( - GenServerName string - NodeName string - Cookie string - err error - - EnableRPC bool -) - -func (dgs *demoGenServ) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleCast: %#v\n", message) - switch message { - case etf.Atom("stop"): - return gen.ServerStatusStopWithReason("stop they said") - case "test": - node := process.Env(node.EnvKeyNode).(node.Node) - n := node.Nodes() - fmt.Println("nodes: ", n) - if err := node.Disconnect(n[0]); err != nil { - fmt.Println("Cant disconnect", err) - } - if err := node.Connect(n[0]); err != nil { - fmt.Println("Cant connect", err) - } - } - return gen.ServerStatusOK -} - -func (dgs *demoGenServ) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - fmt.Printf("HandleCall: %#v, From: %#v\n", message, from) - - switch message { - case etf.Atom("hello"): - process.Cast(process.Self(), "test") - return etf.Term("hi"), gen.ServerStatusOK - } - reply := etf.Tuple{etf.Atom("error"), etf.Atom("unknown_request")} - return reply, gen.ServerStatusOK -} - -func init() { - flag.StringVar(&GenServerName, "gen_server_name", "example", "gen_server name") - flag.StringVar(&NodeName, "name", "demo@127.0.0.1", "node name") - flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") -} - -func main() { - flag.Parse() - - opts := node.Options{ - // enables TLS encryption with self-signed certificate - TLS: node.TLS{Enable: true}, - } - - // Initialize new node with given name, cookie, listening port range and epmd port - nodeTLS, _ := ergo.StartNode(NodeName, Cookie, opts) - - // Spawn process with one arguments - process, _ := nodeTLS.Spawn(GenServerName, gen.ProcessOptions{}, &demoGenServ{}) - fmt.Println("Run erl shell:") - fmt.Printf("erl -proto_dist inet_tls -ssl_dist_opt server_certfile example.crt -ssl_dist_opt server_keyfile example.key -name %s -setcookie %s\n", "erl-"+nodeTLS.Name(), Cookie) - - fmt.Println("-----Examples that can be tried from 'erl'-shell") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", GenServerName, NodeName) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", GenServerName, NodeName) - - process.Wait() - nodeTLS.Stop() -} diff --git a/examples/proxy/demo.png b/examples/proxy/demo.png index 8c0a9694..c770f76c 100644 Binary files a/examples/proxy/demo.png and b/examples/proxy/demo.png differ diff --git a/examples/proxy/demo.svg b/examples/proxy/demo.svg index d35469ca..1aaefb24 100644 --- a/examples/proxy/demo.svg +++ b/examples/proxy/demo.svg @@ -8,7 +8,10 @@ version="1.1" id="svg10716" sodipodi:docname="demo.svg" - inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" + inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" + inkscape:export-filename="demo.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,8 +27,8 @@ inkscape:document-units="mm" showgrid="false" inkscape:zoom="1.5554293" - inkscape:cx="400.85397" - inkscape:cy="261.6641" + inkscape:cx="373.20886" + inkscape:cy="262.307" inkscape:window-width="2788" inkscape:window-height="1196" inkscape:window-x="26" @@ -35,7 +38,9 @@ fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" - fit-margin-bottom="0" /> + fit-margin-bottom="0" + inkscape:showpageshadow="2" + inkscape:deskcolor="#d1d1d1" /> + 104 { - return gen.ServerStatusStop - } - // sending message with delay 1 second - process.SendAfter(process.Self(), value+1, time.Second) - return gen.ServerStatusOK -} - -func main() { - // create a new node - node, _ := ergo.StartNode("node@localhost", "cookies", node.Options{}) - - // spawn a new process of gen.Server - process, _ := node.Spawn("gs1", gen.ProcessOptions{}, &simple{}) - - // send a message to itself - process.Send(process.Self(), 100) - - // wait for the process termination. - process.Wait() - fmt.Println("exited") - node.Stop() -} diff --git a/examples/supervisor/README.md b/examples/supervisor/README.md new file mode 100644 index 00000000..cf6eb413 --- /dev/null +++ b/examples/supervisor/README.md @@ -0,0 +1,34 @@ +## Supervisor demo scenario ## + +``` + demoSup + | + - demoServer01 + - demoServer02 + - demoServer03 +``` + +Here is output of this example: +``` +❯❯❯❯ go run . + +to stop press Ctrl-C + +Started new process + Pid: <32747620.0.1012> + Name: "demoServer01" + Parent: <32747620.0.1011> + Args:[]etf.Term(nil) +Started new process + Pid: <32747620.0.1013> + Name: "demoServer02" + Parent: <32747620.0.1011> + Args:[]etf.Term{12345} +Started new process + Pid: <32747620.0.1014> + Name: "demoServer03" + Parent: <32747620.0.1011> + Args:[]etf.Term{"abc", 67890} +Started supervisor process <32747620.0.1011> + +``` diff --git a/examples/supervisor/demoSupervisor.go b/examples/supervisor/demoSupervisor.go deleted file mode 100644 index 4a192116..00000000 --- a/examples/supervisor/demoSupervisor.go +++ /dev/null @@ -1,118 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "github.com/ergo-services/ergo" - "github.com/ergo-services/ergo/etf" - "github.com/ergo-services/ergo/gen" - "github.com/ergo-services/ergo/node" -) - -var ( - NodeName string - Cookie string - err error - - EnableRPC bool -) - -type demoSup struct { - gen.Supervisor -} - -func (ds *demoSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { - return gen.SupervisorSpec{ - Name: "demoSupervisorSup", - Children: []gen.SupervisorChildSpec{ - gen.SupervisorChildSpec{ - Name: "demoServer01", - Child: &demoGenServ{}, - }, - gen.SupervisorChildSpec{ - Name: "demoServer02", - Child: &demoGenServ{}, - Args: []etf.Term{12345}, - }, - gen.SupervisorChildSpec{ - Name: "demoServer03", - Child: &demoGenServ{}, - Args: []etf.Term{"abc", 67890}, - }, - }, - Strategy: gen.SupervisorStrategy{ - Type: gen.SupervisorStrategyOneForAll, - // Type: gen.SupervisorStrategyRestForOne, - // Type: gen.SupervisorStrategyOneForOne, - Intensity: 2, - Period: 5, - // Restart: gen.SupervisorStrategyRestartTemporary, - // Restart: gen.SupervisorStrategyRestartTransient, - Restart: gen.SupervisorStrategyRestartPermanent, - }, - }, nil -} - -// GenServer implementation structure -type demoGenServ struct { - gen.Server -} - -func (dgs *demoGenServ) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleCast (%s): %#v\n", process.Name(), message) - switch message { - case etf.Atom("stop"): - return gen.ServerStatusStopWithReason("stop they said") - } - return gen.ServerStatusOK -} - -func (dgs *demoGenServ) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { - - if message == etf.Atom("hello") { - return etf.Atom("hi"), gen.ServerStatusOK - } - return etf.Tuple{etf.Atom("error"), etf.Atom("unknown_request")}, gen.ServerStatusOK -} - -func (dgs *demoGenServ) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { - fmt.Printf("HandleInfo (%s): %#v\n", process.Name(), message) - return gen.ServerStatusOK -} - -func (dgs *demoGenServ) Terminate(process *gen.ServerProcess, reason string) { - fmt.Printf("Terminate (%s): %#v\n", process.Name(), reason) -} - -func init() { - flag.StringVar(&NodeName, "name", "demo@127.0.0.1", "node name") - flag.StringVar(&Cookie, "cookie", "123", "cookie for interaction with erlang cluster") -} - -func main() { - flag.Parse() - - // Initialize new node with given name, cookie, listening port range and epmd port - node, _ := ergo.StartNode(NodeName, Cookie, node.Options{}) - - // Spawn supervisor process - process, _ := node.Spawn("demo_sup", gen.ProcessOptions{}, &demoSup{}) - - fmt.Println("Run erl shell:") - fmt.Printf("erl -name %s -setcookie %s\n", "erl-"+node.Name(), Cookie) - - fmt.Println("-----Examples that can be tried from 'erl'-shell") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", "demoServer01", NodeName) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", "demoServer01", NodeName) - fmt.Println("or...") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", "demoServer02", NodeName) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", "demoServer02", NodeName) - fmt.Println("or...") - fmt.Printf("gen_server:cast({%s,'%s'}, stop).\n", "demoServer03", NodeName) - fmt.Printf("gen_server:call({%s,'%s'}, hello).\n", "demoServer03", NodeName) - - process.Wait() - node.Stop() - node.Wait() -} diff --git a/examples/supervisor/main.go b/examples/supervisor/main.go new file mode 100644 index 00000000..086e8a05 --- /dev/null +++ b/examples/supervisor/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +func main() { + flag.Parse() + + fmt.Println("") + fmt.Println("to stop press Ctrl-C") + fmt.Println("") + + demoNode, err := ergo.StartNode("sup@localhost", "cookie", node.Options{}) + if err != nil { + panic(err) + } + + demoSup := createDemoSup() + sup, err := demoNode.Spawn("demoSup", gen.ProcessOptions{}, demoSup) + if err != nil { + panic(err) + } + fmt.Println("Started supervisor process", sup.Self()) + demoNode.Wait() +} diff --git a/examples/supervisor/server.go b/examples/supervisor/server.go new file mode 100644 index 00000000..22da0f07 --- /dev/null +++ b/examples/supervisor/server.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +func createDemoServer() gen.ServerBehavior { + return &demoServer{} +} + +type demoServer struct { + gen.Server +} + +func (ds *demoServer) Init(process *gen.ServerProcess, args ...etf.Term) error { + fmt.Printf("Started new process\n\tPid: %s\n\tName: %q\n\tParent: %s\n\tArgs:%#v\n", + process.Self(), + process.Name(), + process.Parent().Self(), + args) + return nil +} diff --git a/examples/supervisor/sup.go b/examples/supervisor/sup.go new file mode 100644 index 00000000..ae9f9d70 --- /dev/null +++ b/examples/supervisor/sup.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" +) + +func createDemoSup() gen.SupervisorBehavior { + return &demoSup{} +} + +type demoSup struct { + gen.Supervisor +} + +func (ds *demoSup) Init(args ...etf.Term) (gen.SupervisorSpec, error) { + spec := gen.SupervisorSpec{ + Name: "demoAppSup", + Children: []gen.SupervisorChildSpec{ + gen.SupervisorChildSpec{ + Name: "demoServer01", + Child: createDemoServer(), + }, + gen.SupervisorChildSpec{ + Name: "demoServer02", + Child: createDemoServer(), + Args: []etf.Term{12345}, + }, + gen.SupervisorChildSpec{ + Name: "demoServer03", + Child: createDemoServer(), + Args: []etf.Term{"abc", 67890}, + }, + }, + Strategy: gen.SupervisorStrategy{ + Type: gen.SupervisorStrategyOneForAll, + Intensity: 2, + Period: 5, + Restart: gen.SupervisorStrategyRestartTemporary, + }, + } + return spec, nil +} diff --git a/gen/README.md b/gen/README.md index 58df1761..d2967f9f 100644 --- a/gen/README.md +++ b/gen/README.md @@ -12,18 +12,33 @@ A supervisor is responsible for starting, stopping, and monitoring its child pro ### Application Generic application behavior. +### Web + Web API Gateway behavior. + + The Web API Gateway pattern is also sometimes known as the "Backend For Frontend" (BFF) because you build it while thinking about the needs of the client app. Therefore, BFF sits between the client apps and the microservices. It acts as a reverse proxy, routing requests from clients to services. Here is example [examples/genweb](/examples/genweb). + +### TCP + Socket acceptor pool for TCP protocols. + + This behavior aims to provide everything you need to accept TCP connections and process packets with a small code base and low latency while being easy to use. + +### UDP + UDP acceptor pool for UDP protocols + + This behavior provides the same feature set as TCP but for handling UDP packets using pool of handlers. + ### Stage Generic stage behavior (originated from Elixir's [GenStage](https://hexdocs.pm/gen_stage/GenStage.html)). -This is abstraction built on top of `gen.Server` to provide a simple way to create a distributed Producer/Consumer architecture, while automatically managing the concept of backpressure. This implementation is fully compatible with Elixir's GenStage. Example is here [examples/genstage](examples/genstage) or just run `go run ./examples/genstage` to see it in action +This is abstraction built on top of `gen.Server` to provide a simple way to create a distributed Producer/Consumer architecture, while automatically managing the concept of backpressure. This implementation is fully compatible with Elixir's GenStage. Example is here [examples/genstage](/examples/genstage) or just run `go run ./examples/genstage` to see it in action ### Saga Generic saga behavior. -It implements Saga design pattern - a sequence of transactions that updates each service state and publishes the result (or cancels the transaction or triggers the next transaction step). `gen.Saga` also provides a feature of interim results (can be used as transaction progress or as a part of pipeline processing), time deadline (to limit transaction lifespan), two-phase commit (to make distributed transaction atomic). Here is example [examples/gensaga](examples/gensaga). +It implements Saga design pattern - a sequence of transactions that updates each service state and publishes the result (or cancels the transaction or triggers the next transaction step). `gen.Saga` also provides a feature of interim results (can be used as transaction progress or as a part of pipeline processing), time deadline (to limit transaction lifespan), two-phase commit (to make distributed transaction atomic). Here is example [examples/gensaga](/examples/gensaga). ### Raft Generic raft behavior. -It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/raft](examples/raft). +It's improved implementation of [Raft consensus algorithm](https://raft.github.io). The key improvement is using quorum under the hood to manage the leader election process and make the Raft cluster more reliable. This implementation supports quorums of 3, 5, 7, 9, or 11 quorum members. Here is an example of this feature [examples/raft](/examples/genraft). diff --git a/gen/application.go b/gen/application.go index 7dffdfcc..f9acf48d 100644 --- a/gen/application.go +++ b/gen/application.go @@ -50,7 +50,7 @@ type ApplicationSpec struct { Version string Lifespan time.Duration Applications []string - Environment map[EnvKey]interface{} + Env map[EnvKey]interface{} Children []ApplicationChildSpec Process Process StartType ApplicationStartType @@ -78,6 +78,7 @@ type ApplicationInfo struct { // ProcessInit func (a *Application) ProcessInit(p Process, args ...etf.Term) (ProcessState, error) { + spec := p.Env(EnvKeySpec).(*ApplicationSpec) spec, ok := p.Env(EnvKeySpec).(*ApplicationSpec) if !ok { return ProcessState{}, fmt.Errorf("ProcessInit: not an ApplicationBehavior") @@ -87,8 +88,8 @@ func (a *Application) ProcessInit(p Process, args ...etf.Term) (ProcessState, er p.SetTrapExit(true) - if spec.Environment != nil { - for k, v := range spec.Environment { + if spec.Env != nil { + for k, v := range spec.Env { p.SetEnv(k, v) } } @@ -195,14 +196,10 @@ func (a *Application) ProcessLoop(ps ProcessState, started chan<- bool) string { pids = append(pids, spec.Children[i].process.Self()) } - direct.Message = pids - direct.Err = nil - direct.Reply <- direct + ps.PutSyncReply(direct.Ref, pids, nil) default: - direct.Message = nil - direct.Err = ErrUnsupportedRequest - direct.Reply <- direct + ps.PutSyncReply(direct.Ref, nil, lib.ErrUnsupportedRequest) } case <-ps.Context().Done(): diff --git a/gen/raft.go b/gen/raft.go index 9601f6e6..95318612 100644 --- a/gen/raft.go +++ b/gen/raft.go @@ -1,7 +1,6 @@ package gen import ( - "context" "fmt" "math/rand" "sort" @@ -115,7 +114,7 @@ type RaftProcess struct { round int // "log term" in terms of Raft spec // get requests - requests map[etf.Ref]context.CancelFunc + requests map[etf.Ref]CancelFunc // append requests requestsAppend map[string]*requestAppend @@ -123,7 +122,7 @@ type RaftProcess struct { // leader sends heartbeat messages and keep the last sending timestamp heartbeatLeader int64 - heartbeatCancel context.CancelFunc + heartbeatCancel CancelFunc } type leaderElection struct { @@ -132,7 +131,7 @@ type leaderElection struct { round int leader etf.Pid // leader elected voted int // number of peers voted for the leader - cancel context.CancelFunc + cancel CancelFunc } type requestAppend struct { @@ -141,7 +140,7 @@ type requestAppend struct { origin etf.Pid value etf.Term peers map[etf.Pid]bool - cancel context.CancelFunc + cancel CancelFunc } type requestAppendQueued struct { @@ -526,7 +525,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$cluster_join"): join := &messageRaftClusterJoin{} if err := etf.TermIntoStruct(m.Command, &join); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if join.ID != rp.options.ID { @@ -579,7 +578,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { reply := &messageRaftClusterJoinReply{} if err := etf.TermIntoStruct(m.Command, &reply); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if reply.ID != rp.options.ID { @@ -646,7 +645,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$quorum_vote"): vote := &messageRaftQuorumVote{} if err := etf.TermIntoStruct(m.Command, &vote); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if vote.ID != rp.options.ID { // ignore this request @@ -657,7 +656,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$quorum_built"): built := &messageRaftQuorumBuilt{} if err := etf.TermIntoStruct(m.Command, &built); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } // QUODBG fmt.Println(rp.Name(), "GOT QUO BUILT from", m.Pid) if built.ID != rp.options.ID { @@ -753,7 +752,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$leader_heartbeat"): heartbeat := &messageRaftLeaderHeartbeat{} if err := etf.TermIntoStruct(m.Command, &heartbeat); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != heartbeat.ID { @@ -775,7 +774,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$quorum_leave"): leave := &messageRaftQuorumLeave{} if err := etf.TermIntoStruct(m.Command, &leave); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.quorum == nil { return RaftStatusOK @@ -805,7 +804,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$leader_vote"): vote := &messageRaftLeaderVote{} if err := etf.TermIntoStruct(m.Command, &vote); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != vote.ID { @@ -994,7 +993,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$leader_elected"): elected := &messageRaftLeaderElected{} if err := etf.TermIntoStruct(m.Command, &elected); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != elected.ID { @@ -1094,7 +1093,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_get"): requestGet := &messageRaftRequestGet{} if err := etf.TermIntoStruct(m.Command, &requestGet); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != requestGet.ID { @@ -1193,7 +1192,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_reply"): requestReply := &messageRaftRequestReply{} if err := etf.TermIntoStruct(m.Command, &requestReply); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != requestReply.ID { @@ -1217,7 +1216,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_append"): requestAppend := &messageRaftRequestAppend{} if err := etf.TermIntoStruct(m.Command, &requestAppend); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != requestAppend.ID { @@ -1307,7 +1306,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_append_ready"): appendReady := &messageRaftAppendReady{} if err := etf.TermIntoStruct(m.Command, &appendReady); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != appendReady.ID { @@ -1422,7 +1421,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_append_commit"): appendCommit := &messageRaftAppendCommit{} if err := etf.TermIntoStruct(m.Command, &appendCommit); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != appendCommit.ID { @@ -1460,7 +1459,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { case etf.Atom("$request_append_broadcast"): broadcast := &messageRaftAppendBroadcast{} if err := etf.TermIntoStruct(m.Command, &broadcast); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } if rp.options.ID != broadcast.ID { @@ -1474,7 +1473,7 @@ func (rp *RaftProcess) handleRaftRequest(m messageRaft) error { } - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } func (rp *RaftProcess) handleElectionStart(round int) { @@ -2249,7 +2248,7 @@ func (r *Raft) Init(process *ServerProcess, args ...etf.Term) error { behavior: behavior, quorumCandidates: createQuorumCandidates(), quorumVotes: make(map[RaftQuorumState]*quorum), - requests: make(map[etf.Ref]context.CancelFunc), + requests: make(map[etf.Ref]CancelFunc), requestsAppend: make(map[string]*requestAppend), } @@ -2389,7 +2388,7 @@ func (r *Raft) HandleCast(process *ServerProcess, message etf.Term) ServerStatus return ServerStatusOK } status = rp.handleRaftRequest(mRaft) - if status == ErrUnsupportedRequest { + if status == lib.ErrUnsupportedRequest { status = rp.behavior.HandleRaftCast(rp, message) } } @@ -2399,7 +2398,7 @@ func (r *Raft) HandleCast(process *ServerProcess, message etf.Term) ServerStatus return ServerStatusOK case RaftStatusStop: return ServerStatusStop - case ErrUnsupportedRequest: + case lib.ErrUnsupportedRequest: return rp.behavior.HandleRaftInfo(rp, message) default: return ServerStatus(status) @@ -2505,7 +2504,7 @@ func (r *Raft) HandleRaftInfo(process *RaftProcess, message etf.Term) ServerStat // HandleRaftDirect func (r *Raft) HandleRaftDirect(process *RaftProcess, message interface{}) (interface{}, error) { - return nil, ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } // diff --git a/gen/saga.go b/gen/saga.go index 5fdf57bb..71739995 100644 --- a/gen/saga.go +++ b/gen/saga.go @@ -1,7 +1,6 @@ package gen import ( - "context" "fmt" "math" "sync" @@ -73,7 +72,7 @@ type SagaBehavior interface { HandleSagaInfo(process *SagaProcess, message etf.Term) ServerStatus // HandleSagaDirect this callback is invoked on Process.Direct. This method is optional // for the implementation - HandleSagaDirect(process *SagaProcess, message interface{}) (interface{}, error) + HandleSagaDirect(process *SagaProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) } const ( @@ -169,7 +168,7 @@ type SagaTransaction struct { parents []etf.Pid // sagas trace done bool // do not allow send result more than once if 2PC is set - cancelTimer context.CancelFunc + cancelTimer CancelFunc } // SagaNextID @@ -194,7 +193,7 @@ type SagaNext struct { // internal done bool // for 2PC case - cancelTimer context.CancelFunc + cancelTimer CancelFunc } // SagaJobID @@ -218,7 +217,7 @@ type SagaJob struct { commit bool worker Process done bool - cancelTimer context.CancelFunc + cancelTimer CancelFunc } // SagaJobOptions @@ -284,7 +283,7 @@ type sagaSetMaxTransactions struct { // SetMaxTransactions set maximum transactions fo the saga func (gs *Saga) SetMaxTransactions(process Process, max uint) error { if !process.IsAlive() { - return ErrServerTerminated + return lib.ErrServerTerminated } message := sagaSetMaxTransactions{ max: max, @@ -493,8 +492,6 @@ func (sp *SagaProcess) SendResult(id SagaTransactionID, result interface{}) erro }, } - //fmt.Printf("SAGA RESULT %#v\n", message) - // send message to the parent saga if err := sp.Send(tx.parents[0], message); err != nil { return err @@ -633,7 +630,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { nextMessage := messageSagaNext{} if err := etf.TermIntoStruct(m.Command, &nextMessage); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } // Check if exceed the number of transaction on this saga @@ -725,7 +722,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { case "$saga_cancel": cancel := messageSagaCancel{} if err := etf.TermIntoStruct(m.Command, &cancel); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } tx, exist := sp.txs[SagaTransactionID(cancel.TransactionID)] @@ -776,7 +773,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { case etf.Atom("$saga_result"): result := messageSagaResult{} if err := etf.TermIntoStruct(m.Command, &result); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } transactionID := SagaTransactionID(result.TransactionID) @@ -827,7 +824,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { case etf.Atom("$saga_interim"): interim := messageSagaResult{} if err := etf.TermIntoStruct(m.Command, &interim); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } next_id := SagaNextID(interim.Origin) sp.mutexNext.Lock() @@ -853,7 +850,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { // propagate Commit signal if 2PC is enabled commit := messageSagaCommit{} if err := etf.TermIntoStruct(m.Command, &commit); err != nil { - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } transactionID := SagaTransactionID(commit.TransactionID) sp.mutexTXS.Lock() @@ -871,7 +868,7 @@ func (sp *SagaProcess) handleSagaRequest(m messageSaga) error { } return SagaStatusOK } - return ErrUnsupportedRequest + return lib.ErrUnsupportedRequest } func (sp *SagaProcess) cancelTX(from etf.Pid, cancel messageSagaCancel, tx *SagaTransaction) { @@ -1158,14 +1155,14 @@ func (gs *Saga) HandleCall(process *ServerProcess, from ServerFrom, message etf. } // HandleDirect -func (gs *Saga) HandleDirect(process *ServerProcess, message interface{}) (interface{}, error) { +func (gs *Saga) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { sp := process.State.(*SagaProcess) switch m := message.(type) { case sagaSetMaxTransactions: sp.options.MaxTransactions = m.max - return nil, nil + return nil, DirectStatusOK default: - return sp.behavior.HandleSagaDirect(sp, message) + return sp.behavior.HandleSagaDirect(sp, ref, message) } } @@ -1278,7 +1275,7 @@ func (gs *Saga) HandleInfo(process *ServerProcess, message etf.Term) ServerStatu return ServerStatusOK case SagaStatusStop: return ServerStatusStop - case ErrUnsupportedRequest: + case lib.ErrUnsupportedRequest: return sp.behavior.HandleSagaInfo(sp, message) default: return ServerStatus(status) @@ -1325,8 +1322,8 @@ func (gs *Saga) HandleSagaInfo(process *SagaProcess, message etf.Term) ServerSta } // HandleSagaDirect -func (gs *Saga) HandleSagaDirect(process *SagaProcess, message interface{}) (interface{}, error) { - return nil, ErrUnsupportedRequest +func (gs *Saga) HandleSagaDirect(process *SagaProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { + return nil, lib.ErrUnsupportedRequest } // HandleJobResult diff --git a/gen/saga_worker.go b/gen/saga_worker.go index 316cec1f..cce94db2 100644 --- a/gen/saga_worker.go +++ b/gen/saga_worker.go @@ -35,7 +35,7 @@ type SagaWorkerBehavior interface { HandleWorkerCall(process *SagaWorkerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) // HandleWorkerDirect this callback is invoked on Process.Direct. This method is optional // for the implementation - HandleWorkerDirect(process *SagaWorkerProcess, message interface{}) (interface{}, error) + HandleWorkerDirect(process *SagaWorkerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) // HandleWorkerTerminate this callback invoked on a process termination HandleWorkerTerminate(process *SagaWorkerProcess, reason string) @@ -179,9 +179,9 @@ func (w *SagaWorker) HandleCall(process *ServerProcess, from ServerFrom, message } // HandleDirect -func (w *SagaWorker) HandleDirect(process *ServerProcess, message interface{}) (interface{}, error) { +func (w *SagaWorker) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { p := process.State.(*SagaWorkerProcess) - return p.behavior.HandleWorkerDirect(p, message) + return p.behavior.HandleWorkerDirect(p, ref, message) } // HandleInfo @@ -224,9 +224,9 @@ func (w *SagaWorker) HandleWorkerCall(process *SagaWorkerProcess, from ServerFro } // HandleWorkerDirect -func (w *SagaWorker) HandleWorkerDirect(process *SagaWorkerProcess, message interface{}) (interface{}, error) { +func (w *SagaWorker) HandleWorkerDirect(process *SagaWorkerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { lib.Warning("HandleWorkerDirect: unhandled message %#v", message) - return nil, nil + return nil, DirectStatusOK } // HandleWorkerTerminate diff --git a/gen/server.go b/gen/server.go index 9e6063f5..a59211dd 100644 --- a/gen/server.go +++ b/gen/server.go @@ -1,7 +1,6 @@ package gen import ( - "context" "fmt" "runtime" "time" @@ -18,6 +17,8 @@ const ( type ServerBehavior interface { ProcessBehavior + // methods below are optional + // Init invoked on a start Server Init(process *ServerProcess, args ...etf.Term) error @@ -30,7 +31,7 @@ type ServerBehavior interface { HandleCall(process *ServerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) // HandleDirect invoked on a direct request made with Process.Direct - HandleDirect(process *ServerProcess, message interface{}) (interface{}, error) + HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) // HandleInfo invoked if Server received message sent with Process.Send. HandleInfo(process *ServerProcess, message etf.Term) ServerStatus @@ -42,11 +43,15 @@ type ServerBehavior interface { // ServerStatus type ServerStatus error +type DirectStatus error var ( ServerStatusOK ServerStatus = nil ServerStatusStop ServerStatus = fmt.Errorf("stop") ServerStatusIgnore ServerStatus = fmt.Errorf("ignore") + + DirectStatusOK DirectStatus = nil + DirectStatusIgnore DirectStatus = fmt.Errorf("ignore") ) // ServerStatusStopWithReason @@ -55,7 +60,9 @@ func ServerStatusStopWithReason(s string) ServerStatus { } // Server is implementation of ProcessBehavior interface for Server objects -type Server struct{} +type Server struct { + ServerBehavior +} // ServerFrom type ServerFrom struct { @@ -71,7 +78,6 @@ type ServerProcess struct { behavior ServerBehavior counter uint64 // total number of processed messages from mailBox currentFunction string - trapExit bool mailbox <-chan ProcessMailboxMessage original <-chan ProcessMailboxMessage @@ -96,7 +102,7 @@ type handleInfoMessage struct { } // CastAfter a simple wrapper for Process.SendAfter to send a message in fashion of 'gen_server:cast' -func (sp *ServerProcess) CastAfter(to interface{}, message etf.Term, after time.Duration) context.CancelFunc { +func (sp *ServerProcess) CastAfter(to interface{}, message etf.Term, after time.Duration) CancelFunc { msg := etf.Term(etf.Tuple{etf.Atom("$gen_cast"), message}) return sp.SendAfter(to, msg, after) } @@ -190,6 +196,14 @@ func (sp *ServerProcess) SendReply(from ServerFrom, reply etf.Term) error { return sp.Send(to, rep) } +// Reply the handling process.Direct(...) calls can be done asynchronously +// using gen.DirectStatusIgnore as a returning status in the HandleDirect callback. +// In this case, you must reply manualy using gen.ServerProcess.Reply method in any other +// callback. If a caller has canceled this request due to timeout it returns lib.ErrReferenceUnknown +func (sp *ServerProcess) Reply(ref etf.Ref, reply etf.Term, err error) error { + return sp.PutSyncReply(ref, reply, err) +} + // MessageCounter returns the total number of messages handled by Server callbacks: HandleCall, // HandleCast, HandleInfo, HandleDirect func (sp *ServerProcess) MessageCounter() uint64 { @@ -256,7 +270,7 @@ func (gs *Server) ProcessLoop(ps ProcessState, started chan<- bool) string { select { case ex := <-channels.GracefulExit: - if !sp.TrapExit() { + if sp.TrapExit() == false { sp.behavior.Terminate(sp, ex.Reason) return ex.Reason } @@ -305,7 +319,7 @@ func (gs *Server) ProcessLoop(ps ProcessState, started chan<- bool) string { if len(m) != 2 { break } - sp.PutSyncReply(mtag, m.Element(2)) + sp.PutSyncReply(mtag, m.Element(2), nil) if sp.waitReply != nil && *sp.waitReply == mtag { sp.waitReply = nil // continue read sp.callbackWaitReply channel @@ -493,18 +507,16 @@ func (sp *ServerProcess) handleDirect(direct ProcessDirectMessage) { defer sp.panicHandler() } - reply, err := sp.behavior.HandleDirect(sp, direct.Message) - if err != nil { - direct.Message = nil - direct.Err = err - direct.Reply <- direct + cf := sp.currentFunction + sp.currentFunction = "Server:HandleDirect" + reply, status := sp.behavior.HandleDirect(sp, direct.Ref, direct.Message) + sp.currentFunction = cf + switch status { + case DirectStatusIgnore: return + default: + sp.PutSyncReply(direct.Ref, reply, status) } - - direct.Message = reply - direct.Err = nil - direct.Reply <- direct - return } func (sp *ServerProcess) handleCall(m handleCallMessage) { @@ -590,8 +602,8 @@ func (gs *Server) HandleCall(process *ServerProcess, from ServerFrom, message et } // HandleDirect -func (gs *Server) HandleDirect(process *ServerProcess, message interface{}) (interface{}, error) { - return nil, ErrUnsupportedRequest +func (gs *Server) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { + return nil, lib.ErrUnsupportedRequest } // HandleInfo diff --git a/gen/stage.go b/gen/stage.go index 1b394f53..db959042 100644 --- a/gen/stage.go +++ b/gen/stage.go @@ -89,7 +89,7 @@ type StageBehavior interface { HandleStageCall(process *StageProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) // HandleStageDirect this callback is invoked on Process.Direct. This method is optional // for the implementation - HandleStageDirect(process *StageProcess, message interface{}) (interface{}, error) + HandleStageDirect(process *StageProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) // HandleStageCast this callback is invoked on ServerProcess.Cast. This method is optional // for the implementation HandleStageCast(process *StageProcess, message etf.Term) ServerStatus @@ -433,6 +433,7 @@ func (gst *Stage) Init(process *ServerProcess, args ...etf.Term) error { // do not inherit parent State stageProcess.State = nil + behavior := process.Behavior().(StageBehavior) behavior, ok := process.Behavior().(StageBehavior) if !ok { return fmt.Errorf("Stage: not a StageBehavior") @@ -465,9 +466,9 @@ func (gst *Stage) HandleCall(process *ServerProcess, from ServerFrom, message et return stageProcess.behavior.HandleStageCall(stageProcess, from, message) } -func (gst *Stage) HandleDirect(process *ServerProcess, message interface{}) (interface{}, error) { +func (gst *Stage) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { stageProcess := process.State.(*StageProcess) - return stageProcess.behavior.HandleStageDirect(stageProcess, message) + return stageProcess.behavior.HandleStageDirect(stageProcess, ref, message) } func (gst *Stage) HandleCast(process *ServerProcess, message etf.Term) ServerStatus { @@ -528,9 +529,9 @@ func (gst *Stage) HandleStageCall(process *StageProcess, from ServerFrom, messag } // HandleStageDirect -func (gst *Stage) HandleStageDirect(process *StageProcess, message interface{}) (interface{}, error) { +func (gst *Stage) HandleStageDirect(process *StageProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { // default callback if it wasn't implemented - return nil, ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } // HandleStageCast diff --git a/gen/supervisor.go b/gen/supervisor.go index d912a441..d34ccef3 100644 --- a/gen/supervisor.go +++ b/gen/supervisor.go @@ -157,15 +157,7 @@ func (sv *Supervisor) ProcessLoop(ps ProcessState, started chan<- bool) string { case direct := <-chs.Direct: value, err := handleDirect(ps, spec, direct.Message) - if err != nil { - direct.Message = nil - direct.Err = err - direct.Reply <- direct - continue - } - direct.Message = value - direct.Err = nil - direct.Reply <- direct + ps.PutSyncReply(direct.Ref, value, err) case <-chs.Mailbox: // do nothing @@ -225,6 +217,11 @@ func startChild(supervisor Process, name string, child ProcessBehavior, opts Pro if leader := supervisor.GroupLeader(); leader != nil { opts.GroupLeader = leader } + + // Child process shouldn't ignore supervisor termination (via TrapExit). + // Using the supervisor's Context makes the child terminate if the supervisor is terminated. + opts.Context = supervisor.Context() + process, err := supervisor.Spawn(name, opts, child, args...) if err != nil { @@ -267,7 +264,7 @@ func handleDirect(supervisor Process, spec *SupervisorSpec, message interface{}) default: } - return nil, ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func handleMessageExit(p Process, exit ProcessGracefulExitRequest, spec *SupervisorSpec, wait []etf.Pid) []etf.Pid { diff --git a/gen/tcp.go b/gen/tcp.go new file mode 100644 index 00000000..b0fb8d46 --- /dev/null +++ b/gen/tcp.go @@ -0,0 +1,392 @@ +package gen + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net" + "strconv" + "sync/atomic" + "time" + "unsafe" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type TCPBehavior interface { + InitTCP(process *TCPProcess, args ...etf.Term) (TCPOptions, error) + + HandleTCPCall(process *TCPProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleTCPCast(process *TCPProcess, message etf.Term) ServerStatus + HandleTCPInfo(process *TCPProcess, message etf.Term) ServerStatus + + HandleTCPTerminate(process *TCPProcess, reason string) +} + +type TCPStatus error + +var ( + TCPStatusOK TCPStatus + TCPStatusStop TCPStatus = fmt.Errorf("stop") + + defaultDeadlineTimeout int = 3 + defaultDirectTimeout int = 5 +) + +type TCP struct { + Server +} + +type TCPOptions struct { + Host string + Port uint16 + TLS *tls.Config + KeepAlivePeriod int + Handler TCPHandlerBehavior + // QueueLength defines how many parallel requests can be directed to this process. Default value is 10. + QueueLength int + // NumHandlers defines how many handlers will be started. Default 1 + NumHandlers int + // IdleTimeout defines how long (in seconds) keeps the started handler alive with no packets. Zero value makes the handler non-stop. + IdleTimeout int + DeadlineTimeout int + MaxPacketSize int + // ExtraHandlers enables starting new handlers if all handlers in the pool are busy. + ExtraHandlers bool +} + +type TCPProcess struct { + ServerProcess + options TCPOptions + behavior TCPBehavior + + pool []*Process + counter uint64 + listener net.Listener +} + +// +// Server callbacks +// +func (tcp *TCP) Init(process *ServerProcess, args ...etf.Term) error { + + behavior := process.Behavior().(TCPBehavior) + behavior, ok := process.Behavior().(TCPBehavior) + if !ok { + return fmt.Errorf("not a TCPBehavior") + } + + tcpProcess := &TCPProcess{ + ServerProcess: *process, + behavior: behavior, + } + // do not inherit parent State + tcpProcess.State = nil + + options, err := behavior.InitTCP(tcpProcess, args...) + if err != nil { + return err + } + if options.Handler == nil { + return fmt.Errorf("handler must be defined") + } + if options.DeadlineTimeout < 1 { + // we need to check the context if it was canceled to stop + // reading and close the connection socket + options.DeadlineTimeout = defaultDeadlineTimeout + } + + tcpProcess.options = options + if err := tcpProcess.initHandlers(); err != nil { + return err + } + + if options.Port == 0 { + return fmt.Errorf("TCP port must be defined") + } + + lc := net.ListenConfig{} + + if options.KeepAlivePeriod > 0 { + lc.KeepAlive = time.Duration(options.KeepAlivePeriod) * time.Second + } + ctx := process.Context() + hostPort := net.JoinHostPort("", strconv.Itoa(int(options.Port))) + listener, err := lc.Listen(ctx, "tcp", hostPort) + if err != nil { + return err + } + + if options.TLS != nil { + if options.TLS.Certificates == nil && options.TLS.GetCertificate == nil { + return fmt.Errorf("TLS connnnfig has no certificates") + } + listener = tls.NewListener(listener, options.TLS) + } + tcpProcess.listener = listener + + // start acceptor + go func() { + var err error + var c net.Conn + defer func() { + if err == nil { + process.Exit("normal") + return + } + process.Exit(err.Error()) + }() + + for { + c, err = listener.Accept() + if err != nil { + if ctx.Err() == nil { + continue + } + return + } + go tcpProcess.serve(ctx, c) + } + }() + + process.State = tcpProcess + return nil +} + +func (tcp *TCP) Terminate(process *ServerProcess, reason string) { + p := process.State.(*TCPProcess) + p.listener.Close() + p.behavior.HandleTCPTerminate(p, reason) +} + +// +// default TCP callbacks +// + +// HandleTCPCall +func (tcp *TCP) HandleTCPCall(process *TCPProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("[gen.TCP] HandleTCPCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleTCPCast +func (tcp *TCP) HandleTCPCast(process *TCPProcess, message etf.Term) ServerStatus { + lib.Warning("[gen.TCP] HandleTCPCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleTCPInfo +func (tcp *TCP) HandleTCPInfo(process *TCPProcess, message etf.Term) ServerStatus { + lib.Warning("[gen.TCP] HandleTCPInfo: unhandled message %#v", message) + return ServerStatusOK +} +func (tcp *TCP) HandleTCPTerminate(process *TCPProcess, reason string) { + return +} + +// internal + +func (tcpp *TCPProcess) serve(ctx context.Context, c net.Conn) error { + var handlerProcess Process + var handlerProcessID int + var packet interface{} + var disconnect bool + var disconnectError error + var expectingBytes int = 1 + + defer c.Close() + + deadlineTimeout := time.Second * time.Duration(tcpp.options.DeadlineTimeout) + + tcpConnection := &TCPConnection{ + Addr: c.RemoteAddr(), + Socket: c, + } + + l := uint64(tcpp.options.NumHandlers) + // make round robin using the counter value + cnt := atomic.AddUint64(&tcpp.counter, 1) + // choose process as a handler for the packet received on this connection + handlerProcessID = int(cnt % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tcpp.pool[handlerProcessID])))) + + b := lib.TakeBuffer() + +nextPacket: + for { + if ctx.Err() != nil { + return nil + } + + if packet == nil { + // just connected + packet = messageTCPHandlerConnect{ + connection: tcpConnection, + } + break + } + + if b.Len() < expectingBytes { + deadline := false + if err := c.SetReadDeadline(time.Now().Add(deadlineTimeout)); err == nil { + deadline = true + } + + n, e := b.ReadDataFrom(c, tcpp.options.MaxPacketSize) + if n == 0 { + if err, ok := e.(net.Error); deadline && ok && err.Timeout() { + packet = messageTCPHandlerTimeout{ + connection: tcpConnection, + } + break + } + packet = messageTCPHandlerDisconnect{ + connection: tcpConnection, + } + // closed connection + disconnect = true + break + } + + if e != nil && e != io.EOF { + // something went wrong + packet = messageTCPHandlerDisconnect{ + connection: tcpConnection, + } + disconnect = true + disconnectError = e + break + } + + // check onemore time if we should read more data + continue + } + // FIXME take it from the pool + packet = &messageTCPHandlerPacket{ + connection: tcpConnection, + packet: b.B, + } + break + } + +retry: + for a := uint64(0); a < l; a++ { + if ctx.Err() != nil { + return nil + } + + nbytesInt, err := handlerProcess.DirectWithTimeout(packet, defaultDirectTimeout) + switch err { + case TCPHandlerStatusOK: + if disconnect { + return disconnectError + } + next, _ := nbytesInt.(messageTCPHandlerPacketResult) + if next.left > 0 { + if b.Len() > next.left { + b1 := lib.TakeBuffer() + head := b.Len() - next.left + b1.Set(b.B[head:]) + lib.ReleaseBuffer(b) + b = b1 + } + } else { + b.Reset() + } + expectingBytes = b.Len() + next.await + if expectingBytes == 0 { + expectingBytes++ + } + + //fmt.Println("TCP NEXT", next, expectingBytes) + goto nextPacket + + case TCPHandlerStatusClose: + return disconnectError + case lib.ErrProcessTerminated: + if handlerProcessID == -1 { + // it was an extra handler do not restart. try to use the existing one + cnt = atomic.AddUint64(&tcpp.counter, 1) + handlerProcessID = int(cnt % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tcpp.pool[handlerProcessID])))) + goto retry + } + + // respawn terminated process + handlerProcess = tcpp.startHandler(handlerProcessID, tcpp.options.IdleTimeout) + atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&tcpp.pool[handlerProcessID])), unsafe.Pointer(&handlerProcess)) + continue + case lib.ErrProcessBusy: + handlerProcessID = int((a + cnt) % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tcpp.pool[handlerProcessID])))) + continue + default: + lib.Warning("[gen.TCP] error on handling packet: %s. closing connection with %q", err, c.RemoteAddr()) + return err + } + + expectingBytes = 1 + goto nextPacket + } + + // create a new handler. we should eather to make a call HandleDisconnect or + // run this connection with the extra handler with idle timeout = 5 second + handlerProcessID = -1 + handlerProcess = tcpp.startHandler(handlerProcessID, 5) + if tcpp.options.ExtraHandlers == false { + packet = messageTCPHandlerDisconnect{ + connection: tcpConnection, + } + + handlerProcess.DirectWithTimeout(packet, defaultDirectTimeout) + lib.Warning("[gen.TCP] all handlers are busy. closing connection with %q", c.RemoteAddr()) + handlerProcess.Kill() + return fmt.Errorf("all handlers are busy") + } + + goto retry +} + +func (tcpp *TCPProcess) initHandlers() error { + if tcpp.options.NumHandlers < 1 { + tcpp.options.NumHandlers = 1 + } + if tcpp.options.IdleTimeout < 0 { + tcpp.options.IdleTimeout = 0 + } + + if tcpp.options.QueueLength < 1 { + tcpp.options.QueueLength = defaultQueueLength + } + + c := atomic.AddUint64(&tcpp.counter, 1) + if c > 1 { + return fmt.Errorf("you can not use the same object more than once") + } + + for i := 0; i < tcpp.options.NumHandlers; i++ { + p := tcpp.startHandler(i, tcpp.options.IdleTimeout) + if p == nil { + return fmt.Errorf("can not initialize handlers") + } + tcpp.pool = append(tcpp.pool, &p) + } + return nil +} + +func (tcpp *TCPProcess) startHandler(id int, idleTimeout int) Process { + opts := ProcessOptions{ + Context: tcpp.Context(), + DirectboxSize: uint16(tcpp.options.QueueLength), + } + + optsHandler := optsTCPHandler{id: id, idleTimeout: idleTimeout} + p, err := tcpp.Spawn("", opts, tcpp.options.Handler, optsHandler) + if err != nil { + lib.Warning("[gen.TCP] can not start TCPHandler: %s", err) + return nil + } + return p +} diff --git a/gen/tcp_handler.go b/gen/tcp_handler.go new file mode 100644 index 00000000..3151e7aa --- /dev/null +++ b/gen/tcp_handler.go @@ -0,0 +1,207 @@ +package gen + +import ( + "fmt" + "io" + "net" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type TCPHandlerStatus error + +var ( + TCPHandlerStatusOK TCPHandlerStatus = nil + TCPHandlerStatusClose TCPHandlerStatus = fmt.Errorf("close") + + defaultQueueLength = 10 +) + +type TCPHandlerBehavior interface { + ServerBehavior + + // Mandatory callback + HandlePacket(process *TCPHandlerProcess, packet []byte, conn *TCPConnection) (int, int, TCPHandlerStatus) + + // Optional callbacks + HandleConnect(process *TCPHandlerProcess, conn *TCPConnection) TCPHandlerStatus + HandleDisconnect(process *TCPHandlerProcess, conn *TCPConnection) + HandleTimeout(process *TCPHandlerProcess, conn *TCPConnection) TCPHandlerStatus + + HandleTCPHandlerCall(process *TCPHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleTCPHandlerCast(process *TCPHandlerProcess, message etf.Term) ServerStatus + HandleTCPHandlerInfo(process *TCPHandlerProcess, message etf.Term) ServerStatus + HandleTCPHandlerTerminate(process *TCPHandlerProcess, reason string) +} + +type TCPHandler struct { + Server + + behavior TCPHandlerBehavior +} + +type TCPHandlerProcess struct { + ServerProcess + behavior TCPHandlerBehavior + + lastPacket int64 + idleTimeout int + id int +} + +type optsTCPHandler struct { + id int + idleTimeout int +} + +type TCPConnection struct { + Addr net.Addr + Socket io.Writer + State interface{} +} + +type messageTCPHandlerIdleCheck struct{} +type messageTCPHandlerPacket struct { + packet []byte + connection *TCPConnection +} +type messageTCPHandlerPacketResult struct { + left int + await int +} +type messageTCPHandlerConnect struct { + connection *TCPConnection +} +type messageTCPHandlerDisconnect struct { + connection *TCPConnection +} + +type messageTCPHandlerTimeout struct { + connection *TCPConnection +} + +func (tcph *TCPHandler) Init(process *ServerProcess, args ...etf.Term) error { + behavior, ok := process.Behavior().(TCPHandlerBehavior) + if !ok { + return fmt.Errorf("TCP: not a TCPHandlerBehavior") + } + handlerProcess := &TCPHandlerProcess{ + ServerProcess: *process, + behavior: behavior, + } + if len(args) == 0 { + return fmt.Errorf("TCP: can not start with no args") + } + + if a, ok := args[0].(optsTCPHandler); ok { + handlerProcess.idleTimeout = a.idleTimeout + handlerProcess.id = a.id + } else { + return fmt.Errorf("TCP: wrong args for the TCPHandler") + } + + // do not inherit parent State + handlerProcess.State = nil + process.State = handlerProcess + + if handlerProcess.idleTimeout > 0 { + process.CastAfter(process.Self(), messageTCPHandlerIdleCheck{}, 5*time.Second) + } + + return nil +} + +func (tcph *TCPHandler) HandleCall(process *ServerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + tcpp := process.State.(*TCPHandlerProcess) + return tcpp.behavior.HandleTCPHandlerCall(tcpp, from, message) +} + +func (tcph *TCPHandler) HandleCast(process *ServerProcess, message etf.Term) ServerStatus { + tcpp := process.State.(*TCPHandlerProcess) + switch message.(type) { + case messageTCPHandlerIdleCheck: + if time.Now().Unix()-tcpp.lastPacket > int64(tcpp.idleTimeout) { + return ServerStatusStop + } + process.CastAfter(process.Self(), messageTCPHandlerIdleCheck{}, 5*time.Second) + + default: + return tcpp.behavior.HandleTCPHandlerCast(tcpp, message) + } + return ServerStatusOK +} + +func (tcph *TCPHandler) HandleInfo(process *ServerProcess, message etf.Term) ServerStatus { + tcpp := process.State.(*TCPHandlerProcess) + return tcpp.behavior.HandleTCPHandlerInfo(tcpp, message) +} + +func (tcph *TCPHandler) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { + tcpp := process.State.(*TCPHandlerProcess) + switch m := message.(type) { + case *messageTCPHandlerPacket: + tcpp.lastPacket = time.Now().Unix() + left, await, err := tcpp.behavior.HandlePacket(tcpp, m.packet, m.connection) + res := messageTCPHandlerPacketResult{ + left: left, + await: await, + } + return res, err + case messageTCPHandlerConnect: + return nil, tcpp.behavior.HandleConnect(tcpp, m.connection) + case messageTCPHandlerDisconnect: + tcpp.behavior.HandleDisconnect(tcpp, m.connection) + return nil, TCPHandlerStatusClose + case messageTCPHandlerTimeout: + return nil, tcpp.behavior.HandleTimeout(tcpp, m.connection) + default: + return nil, DirectStatusOK + } +} + +func (tcph *TCPHandler) Terminate(process *ServerProcess, reason string) { + tcpp := process.State.(*TCPHandlerProcess) + tcpp.behavior.HandleTCPHandlerTerminate(tcpp, reason) +} + +// +// default callbacks +// + +func (tcph *TCPHandler) HandleConnect(process *TCPHandlerProcess, conn *TCPConnection) TCPHandlerStatus { + return TCPHandlerStatusOK +} +func (tcph *TCPHandler) HandleDisconnect(process *TCPHandlerProcess, conn *TCPConnection) { + return +} +func (tcph *TCPHandler) HandleTimeout(process *TCPHandlerProcess, conn *TCPConnection) TCPHandlerStatus { + return TCPHandlerStatusOK +} + +// HandleTCPHandlerCall +func (tcph *TCPHandler) HandleTCPHandlerCall(process *TCPHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("HandleTCPHandlerCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleTCPHandlerCast +func (tcph *TCPHandler) HandleTCPHandlerCast(process *TCPHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleTCPHandlerCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleTCPHandlerInfo +func (tcph *TCPHandler) HandleTCPHandlerInfo(process *TCPHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleTCPHandlerInfo: unhandled message %#v", message) + return ServerStatusOK +} +func (tcph *TCPHandler) HandleTCPHandlerTerminate(process *TCPHandlerProcess, reason string) { + return +} + +// we should disable SetTrapExit for the TCPHandlerProcess by overriding it. +func (tcpp *TCPHandlerProcess) SetTrapExit(trap bool) { + lib.Warning("[%s] method 'SetTrapExit' is disabled for TCPHandlerProcess", tcpp.Self()) +} diff --git a/gen/types.go b/gen/types.go index d2f12fe3..d2f15551 100644 --- a/gen/types.go +++ b/gen/types.go @@ -8,11 +8,6 @@ import ( "github.com/ergo-services/ergo/etf" ) -var ( - ErrUnsupportedRequest = fmt.Errorf("unsupported request") - ErrServerTerminated = fmt.Errorf("server terminated") -) - // EnvKey type EnvKey string @@ -67,8 +62,9 @@ type Process interface { // SendAfter starts a timer. When the timer expires, the message sends to the process // identified by 'to'. 'to' can be a Pid, registered local name or // gen.ProcessID{RegisteredName, NodeName}. Returns cancel function in order to discard - // sending a message - SendAfter(to interface{}, message etf.Term, after time.Duration) context.CancelFunc + // sending a message. CancelFunc returns bool value. If it returns false, than the timer has + // already expired and the message has been sent. + SendAfter(to interface{}, message etf.Term, after time.Duration) CancelFunc // Exit initiate a graceful stopping process Exit(reason string) error @@ -182,10 +178,17 @@ type Process interface { // Aliases returns list of aliases of this process. Aliases() []etf.Alias - PutSyncRequest(ref etf.Ref) + // RegisterEvent + RegisterEvent(event Event, messages ...EventMessage) error + UnregisterEvent(event Event) error + MonitorEvent(event Event) error + DemonitorEvent(event Event) error + SendEventMessage(event Event, message EventMessage) error + + PutSyncRequest(ref etf.Ref) error CancelSyncRequest(ref etf.Ref) WaitSyncReply(ref etf.Ref, timeout int) (etf.Term, error) - PutSyncReply(ref etf.Ref, term etf.Term) error + PutSyncReply(ref etf.Ref, term etf.Term, err error) error ProcessChannels() ProcessChannels } @@ -209,11 +212,14 @@ type ProcessInfo struct { // ProcessOptions type ProcessOptions struct { - // Context allows mix the system context with the custom one. E.g. to limit - // the lifespan using context.WithTimeout + // Context allows mixing the system context with the custom one. E.g., to limit + // the lifespan using context.WithTimeout. This context MUST be based on the + // other Process' context. Otherwise, you get the error lib.ErrProcessContext Context context.Context // MailboxSize defines the length of message queue for the process MailboxSize uint16 + // DirectboxSize defines the length of message queue for the direct requests + DirectboxSize uint16 // GroupLeader GroupLeader Process // Env set the process environment variables @@ -266,9 +272,9 @@ type ProcessMailboxMessage struct { // ProcessDirectMessage type ProcessDirectMessage struct { + Ref etf.Ref Message interface{} Err error - Reply chan ProcessDirectMessage } // ProcessGracefulExitRequest @@ -452,3 +458,15 @@ func IsMessageFallback(message etf.Term) (MessageFallback, bool) { } return mf, false } + +type CancelFunc func() bool + +type EventMessage interface{} +type Event string + +// MessageEventDown delivers to the process which monitored EventType if the owner +// of this EventType has terminated +type MessageEventDown struct { + Event Event + Reason string +} diff --git a/gen/udp.go b/gen/udp.go new file mode 100644 index 00000000..929f84b4 --- /dev/null +++ b/gen/udp.go @@ -0,0 +1,301 @@ +package gen + +import ( + "fmt" + "io" + "net" + "strconv" + "sync/atomic" + "time" + "unsafe" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type UDPBehavior interface { + InitUDP(process *UDPProcess, args ...etf.Term) (UDPOptions, error) + + HandleUDPCall(process *UDPProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleUDPCast(process *UDPProcess, message etf.Term) ServerStatus + HandleUDPInfo(process *UDPProcess, message etf.Term) ServerStatus + + HandleUDPTerminate(process *UDPProcess, reason string) +} + +type UDPStatus error + +var ( + UDPStatusOK UDPStatus + UDPStatusStop UDPStatus = fmt.Errorf("stop") + + defaultUDPDeadlineTimeout int = 3 + defaultUDPQueueLength int = 10 + defaultUDPMaxPacketSize = int(65000) +) + +type UDP struct { + Server +} + +type UDPOptions struct { + Host string + Port uint16 + + Handler UDPHandlerBehavior + NumHandlers int + IdleTimeout int + DeadlineTimeout int + QueueLength int + MaxPacketSize int + ExtraHandlers bool +} + +type UDPProcess struct { + ServerProcess + options UDPOptions + behavior UDPBehavior + + pool []*Process + counter uint64 + packetConn net.PacketConn +} + +type UDPPacket struct { + Addr net.Addr + Socket io.Writer +} + +// +// Server callbacks +// +func (udp *UDP) Init(process *ServerProcess, args ...etf.Term) error { + + behavior := process.Behavior().(UDPBehavior) + behavior, ok := process.Behavior().(UDPBehavior) + if !ok { + return fmt.Errorf("not a UDPBehavior") + } + + udpProcess := &UDPProcess{ + ServerProcess: *process, + behavior: behavior, + } + // do not inherit parent State + udpProcess.State = nil + + options, err := behavior.InitUDP(udpProcess, args...) + if err != nil { + return err + } + if options.Handler == nil { + return fmt.Errorf("handler must be defined") + } + + if options.QueueLength == 0 { + options.QueueLength = defaultUDPQueueLength + } + + if options.DeadlineTimeout < 1 { + // we need to check the context if it was canceled to stop + // reading and close the connection socket + options.DeadlineTimeout = defaultUDPDeadlineTimeout + } + + if options.MaxPacketSize == 0 { + options.MaxPacketSize = defaultUDPMaxPacketSize + } + + udpProcess.options = options + if err := udpProcess.initHandlers(); err != nil { + return err + } + + if options.Port == 0 { + return fmt.Errorf("UDP port must be defined") + } + + lc := net.ListenConfig{} + hostPort := net.JoinHostPort("", strconv.Itoa(int(options.Port))) + pconn, err := lc.ListenPacket(process.Context(), "udp", hostPort) + if err != nil { + return err + } + + udpProcess.packetConn = pconn + process.State = udpProcess + + // start serving + go udpProcess.serve() + return nil +} +func (udp *UDP) Terminate(process *ServerProcess, reason string) { + p := process.State.(*UDPProcess) + p.packetConn.Close() + p.behavior.HandleUDPTerminate(p, reason) +} + +// +// default UDP callbacks +// + +// HandleUDPCall +func (udp *UDP) HandleUDPCall(process *UDPProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("[gen.UDP] HandleUDPCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleUDPCast +func (udp *UDP) HandleUDPCast(process *UDPProcess, message etf.Term) ServerStatus { + lib.Warning("[gen.UDP] HandleUDPCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleUDPInfo +func (udp *UDP) HandleUDPInfo(process *UDPProcess, message etf.Term) ServerStatus { + lib.Warning("[gen.UDP] HandleUDPInfo: unhandled message %#v", message) + return ServerStatusOK +} +func (udp *UDP) HandleUDPTerminate(process *UDPProcess, reason string) { + return +} + +// internals + +func (udpp *UDPProcess) initHandlers() error { + if udpp.options.NumHandlers < 1 { + udpp.options.NumHandlers = 1 + } + if udpp.options.IdleTimeout < 0 { + udpp.options.IdleTimeout = 0 + } + + c := atomic.AddUint64(&udpp.counter, 1) + if c > 1 { + return fmt.Errorf("you can not use the same object more than once") + } + + for i := 0; i < udpp.options.NumHandlers; i++ { + p := udpp.startHandler(i, udpp.options.IdleTimeout) + if p == nil { + return fmt.Errorf("can not initialize handlers") + } + udpp.pool = append(udpp.pool, &p) + } + return nil +} + +func (udpp *UDPProcess) startHandler(id int, idleTimeout int) Process { + opts := ProcessOptions{ + Context: udpp.Context(), + MailboxSize: uint16(udpp.options.QueueLength), + } + + optsHandler := optsUDPHandler{id: id, idleTimeout: idleTimeout} + p, err := udpp.Spawn("", opts, udpp.options.Handler, optsHandler) + if err != nil { + lib.Warning("[gen.UDP] can not start UDPHandler: %s", err) + return nil + } + return p +} + +func (udpp *UDPProcess) serve() { + var handlerProcess Process + var handlerProcessID int + var packet interface{} + defer udpp.packetConn.Close() + + writer := &writer{ + pconn: udpp.packetConn, + } + + ctx := udpp.Context() + deadlineTimeout := time.Second * time.Duration(udpp.options.DeadlineTimeout) + + l := uint64(udpp.options.NumHandlers) + // make round robin using the counter value + cnt := atomic.AddUint64(&udpp.counter, 1) + // choose process as a handler for the packet received on this connection + handlerProcessID = int(cnt % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&udpp.pool[handlerProcessID])))) + +nextPacket: + for { + if ctx.Err() != nil { + return + } + deadline := false + if err := udpp.packetConn.SetReadDeadline(time.Now().Add(deadlineTimeout)); err == nil { + deadline = true + } + buf := lib.TakeBuffer() + buf.Allocate(udpp.options.MaxPacketSize) + n, a, err := udpp.packetConn.ReadFrom(buf.B) + if n == 0 { + if err, ok := err.(net.Error); deadline && ok && err.Timeout() { + packet = messageUDPHandlerTimeout{} + break + } + // stop serving and close this socket + return + } + if err != nil { + lib.Warning("[gen.UDP] got error on receiving packet from %q: %s", a, err) + } + + writer.addr = a + packet = messageUDPHandlerPacket{ + data: buf, + packet: UDPPacket{ + Addr: a, + Socket: writer, + }, + n: n, + } + break + } + +retry: + for a := uint64(0); a < l; a++ { + if ctx.Err() != nil { + return + } + + err := udpp.Cast(handlerProcess.Self(), packet) + switch err { + case nil: + break + case lib.ErrProcessUnknown: + if handlerProcessID == -1 { + // it was an extra handler do not restart. try to use the existing one + cnt = atomic.AddUint64(&udpp.counter, 1) + handlerProcessID = int(cnt % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&udpp.pool[handlerProcessID])))) + goto retry + } + + // respawn terminated process + handlerProcess = udpp.startHandler(handlerProcessID, udpp.options.IdleTimeout) + atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&udpp.pool[handlerProcessID])), unsafe.Pointer(&handlerProcess)) + continue + + case lib.ErrProcessBusy: + handlerProcessID = int((a + cnt) % l) + handlerProcess = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&udpp.pool[handlerProcessID])))) + continue + default: + lib.Warning("[gen.UDP] error on handling packet %#v: %s", packet, err) + } + goto nextPacket + } +} + +type writer struct { + pconn net.PacketConn + addr net.Addr +} + +func (w *writer) Write(data []byte) (int, error) { + return w.pconn.WriteTo(data, w.addr) +} diff --git a/gen/udp_handler.go b/gen/udp_handler.go new file mode 100644 index 00000000..4d206887 --- /dev/null +++ b/gen/udp_handler.go @@ -0,0 +1,152 @@ +package gen + +import ( + "fmt" + "time" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type UDPHandlerBehavior interface { + ServerBehavior + + // Mandatory callback + HandlePacket(process *UDPHandlerProcess, data []byte, packet UDPPacket) + + // Optional callbacks + HandleTimeout(process *UDPHandlerProcess) + + HandleUDPHandlerCall(process *UDPHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleUDPHandlerCast(process *UDPHandlerProcess, message etf.Term) ServerStatus + HandleUDPHandlerInfo(process *UDPHandlerProcess, message etf.Term) ServerStatus + HandleUDPHandlerTerminate(process *UDPHandlerProcess, reason string) +} + +type UDPHandler struct { + Server +} + +type UDPHandlerProcess struct { + ServerProcess + behavior UDPHandlerBehavior + + lastPacket int64 + idleTimeout int + id int +} + +type optsUDPHandler struct { + id int + idleTimeout int +} +type messageUDPHandlerIdleCheck struct{} +type messageUDPHandlerPacket struct { + data *lib.Buffer + packet UDPPacket + n int +} +type messageUDPHandlerTimeout struct{} + +func (udph *UDPHandler) Init(process *ServerProcess, args ...etf.Term) error { + behavior, ok := process.Behavior().(UDPHandlerBehavior) + if !ok { + return fmt.Errorf("UDP: not a UDPHandlerBehavior") + } + handlerProcess := &UDPHandlerProcess{ + ServerProcess: *process, + behavior: behavior, + } + if len(args) == 0 { + return fmt.Errorf("UDP: can not start with no args") + } + + if a, ok := args[0].(optsUDPHandler); ok { + handlerProcess.idleTimeout = a.idleTimeout + handlerProcess.id = a.id + } else { + return fmt.Errorf("UDP: wrong args for the UDPHandler") + } + + // do not inherit parent State + handlerProcess.State = nil + process.State = handlerProcess + + if handlerProcess.idleTimeout > 0 { + process.CastAfter(process.Self(), messageUDPHandlerIdleCheck{}, 5*time.Second) + } + + return nil +} + +func (udph *UDPHandler) HandleCall(process *ServerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + udpp := process.State.(*UDPHandlerProcess) + return udpp.behavior.HandleUDPHandlerCall(udpp, from, message) +} + +func (udph *UDPHandler) HandleCast(process *ServerProcess, message etf.Term) ServerStatus { + udpp := process.State.(*UDPHandlerProcess) + switch m := message.(type) { + case messageUDPHandlerIdleCheck: + if time.Now().Unix()-udpp.lastPacket > int64(udpp.idleTimeout) { + return ServerStatusStop + } + process.CastAfter(process.Self(), messageUDPHandlerIdleCheck{}, 5*time.Second) + + case messageUDPHandlerPacket: + udpp.lastPacket = time.Now().Unix() + udpp.behavior.HandlePacket(udpp, m.data.B[:m.n], m.packet) + lib.ReleaseBuffer(m.data) + + case messageUDPHandlerTimeout: + udpp.behavior.HandleTimeout(udpp) + + default: + return udpp.behavior.HandleUDPHandlerCast(udpp, message) + } + return ServerStatusOK +} + +func (udph *UDPHandler) HandleInfo(process *ServerProcess, message etf.Term) ServerStatus { + udpp := process.State.(*UDPHandlerProcess) + return udpp.behavior.HandleUDPHandlerInfo(udpp, message) +} + +func (udph *UDPHandler) Terminate(process *ServerProcess, reason string) { + udpp := process.State.(*UDPHandlerProcess) + udpp.behavior.HandleUDPHandlerTerminate(udpp, reason) +} + +// +// default callbacks +// + +func (udph *UDPHandler) HandleTimeout(process *UDPHandlerProcess) { + return +} + +// HandleUDPHandlerCall +func (udph *UDPHandler) HandleUDPHandlerCall(process *UDPHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("HandleUDPHandlerCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleUDPHandlerCast +func (udph *UDPHandler) HandleUDPHandlerCast(process *UDPHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleUDPHandlerCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleUDPHandlerInfo +func (udph *UDPHandler) HandleUDPHandlerInfo(process *UDPHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleUDPHandlerInfo: unhandled message %#v", message) + return ServerStatusOK +} +func (udph *UDPHandler) HandleUDPHandlerTerminate(process *UDPHandlerProcess, reason string) { + return +} + +// we should disable SetTrapExit for the UDPHandlerProcess by overriding it. +func (udpp *UDPHandlerProcess) SetTrapExit(trap bool) { + lib.Warning("[%s] method 'SetTrapExit' is disabled for UDPHandlerProcess", udpp.Self()) +} diff --git a/gen/web.go b/gen/web.go new file mode 100644 index 00000000..bf85e9d4 --- /dev/null +++ b/gen/web.go @@ -0,0 +1,223 @@ +package gen + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "reflect" + "strconv" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +type WebBehavior interface { + // mandatory method + InitWeb(process *WebProcess, args ...etf.Term) (WebOptions, error) + + // optional methods + HandleWebCall(process *WebProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleWebCast(process *WebProcess, message etf.Term) ServerStatus + HandleWebInfo(process *WebProcess, message etf.Term) ServerStatus +} + +type WebStatus error + +var ( + WebStatusOK WebStatus // nil + WebStatusStop WebStatus = fmt.Errorf("stop") + + // internals + defaultWebPort = uint16(8080) + defaultWebTLSPort = uint16(8443) +) + +type Web struct { + Server + WebBehavior +} + +type WebOptions struct { + Host string + Port uint16 // default port 8080, for TLS - 8443 + TLS *tls.Config + Handler http.Handler +} + +type WebProcess struct { + ServerProcess + options WebOptions + behavior WebBehavior + listener net.Listener +} + +type defaultHandler struct{} + +func (dh *defaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "Handler is not initialized\n") +} + +// +// WebProcess API +// + +func (wp *WebProcess) StartWebHandler(web WebHandlerBehavior, options WebHandlerOptions) http.Handler { + handler, err := web.initHandler(wp, web, options) + if err != nil { + name := reflect.ValueOf(web).Elem().Type().Name() + lib.Warning("[%s] can not initialaze WebHandler (%s): %s", wp.Self(), name, err) + + return &defaultHandler{} + } + return handler +} + +// +// Server callbacks +// + +func (web *Web) Init(process *ServerProcess, args ...etf.Term) error { + + behavior, ok := process.Behavior().(WebBehavior) + if !ok { + return fmt.Errorf("Web: not a WebBehavior") + } + + webProcess := &WebProcess{ + ServerProcess: *process, + behavior: behavior, + } + // do not inherit parent State + webProcess.State = nil + + options, err := behavior.InitWeb(webProcess, args...) + if err != nil { + return err + } + + tlsEnabled := false + if options.TLS != nil { + if options.TLS.Certificates == nil && options.TLS.GetCertificate == nil { + return fmt.Errorf("TLS config has no certificates") + } + tlsEnabled = true + } + + if options.Port == 0 { + if tlsEnabled { + options.Port = defaultWebTLSPort + } else { + options.Port = defaultWebPort + } + } + + lc := net.ListenConfig{} + ctx := process.Context() + hostPort := net.JoinHostPort(options.Host, strconv.Itoa(int(options.Port))) + listener, err := lc.Listen(ctx, "tcp", hostPort) + if err != nil { + return err + } + + if tlsEnabled { + listener = tls.NewListener(listener, options.TLS) + } + + httpServer := http.Server{ + Handler: options.Handler, + } + + // start acceptor + go func() { + err := httpServer.Serve(listener) + process.Exit(err.Error()) + }() + + // Golang's listener is weird. It takes the context in the Listen method + // but doesn't use it at all. HTTP server has the same issue. + // So making a little workaround to handle process context cancelation. + // Maybe one day they fix it. + go func() { + // this goroutine will be alive until the process context is canceled. + select { + case <-ctx.Done(): + httpServer.Close() + } + }() + + webProcess.options = options + webProcess.listener = listener + process.State = webProcess + + return nil +} + +// HandleCall +func (web *Web) HandleCall(process *ServerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + webp := process.State.(*WebProcess) + return webp.behavior.HandleWebCall(webp, from, message) +} + +// HandleDirect +func (web *Web) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { + return nil, DirectStatusOK +} + +// HandleCast +func (web *Web) HandleCast(process *ServerProcess, message etf.Term) ServerStatus { + webp := process.State.(*WebProcess) + status := webp.behavior.HandleWebCast(webp, message) + + switch status { + case WebStatusOK: + return ServerStatusOK + case WebStatusStop: + return ServerStatusStop + default: + return ServerStatus(status) + } +} + +// HandleInfo +func (web *Web) HandleInfo(process *ServerProcess, message etf.Term) ServerStatus { + webp := process.State.(*WebProcess) + status := webp.behavior.HandleWebInfo(webp, message) + + switch status { + case WebStatusOK: + return ServerStatusOK + case WebStatusStop: + return ServerStatusStop + default: + return ServerStatus(status) + } +} + +func (web *Web) Terminate(process *ServerProcess, reason string) { + webp := process.State.(*WebProcess) + webp.listener.Close() +} + +// +// default Web callbacks +// + +// HandleWebCall +func (web *Web) HandleWebCall(process *WebProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("HandleWebCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleWebCast +func (web *Web) HandleWebCast(process *WebProcess, message etf.Term) ServerStatus { + lib.Warning("HandleWebCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleWebInfo +func (web *Web) HandleWebInfo(process *WebProcess, message etf.Term) ServerStatus { + lib.Warning("HandleWebInfo: unhandled message %#v", message) + return ServerStatusOK +} diff --git a/gen/web_handler.go b/gen/web_handler.go new file mode 100644 index 00000000..f656d36c --- /dev/null +++ b/gen/web_handler.go @@ -0,0 +1,351 @@ +package gen + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/lib" +) + +var ( + WebHandlerStatusDone WebHandlerStatus = nil + WebHandlerStatusWait WebHandlerStatus = fmt.Errorf("wait") + + defaultRequestQueueLength = 10 + + webMessageRequestPool = &sync.Pool{ + New: func() interface{} { + return &webMessageRequest{} + }, + } +) + +type WebHandlerStatus error + +type WebHandlerBehavior interface { + ServerBehavior + + // Mandatory callback + HandleRequest(process *WebHandlerProcess, request WebMessageRequest) WebHandlerStatus + + // Optional callbacks + HandleWebHandlerCall(process *WebHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) + HandleWebHandlerCast(process *WebHandlerProcess, message etf.Term) ServerStatus + HandleWebHandlerInfo(process *WebHandlerProcess, message etf.Term) ServerStatus + HandleWebHandlerTerminate(process *WebHandlerProcess, reason string, count int64) + + // internal methods + initHandler(process Process, handler WebHandlerBehavior, options WebHandlerOptions) (http.Handler, error) +} + +type WebHandler struct { + Server + + parent Process + behavior WebHandlerBehavior + options WebHandlerOptions + pool []*Process + counter uint64 +} + +type poolItem struct { + process Process +} + +type WebHandlerOptions struct { + // Timeout for web-requests. The default timeout is 5 seconds. It can also be + // overridden within HTTP requests using the header 'Request-Timeout' + RequestTimeout int + // RequestQueueLength defines how many parallel requests can be directed to this process. Default value is 10. + RequestQueueLength int + // NumHandlers defines how many handlers will be started. Default 1 + NumHandlers int + // IdleTimeout defines how long (in seconds) keep the started handler alive with no requests. Zero value makes handler not stop. + IdleTimeout int +} + +type WebHandlerProcess struct { + ServerProcess + behavior WebHandlerBehavior + lastRequest int64 + counter int64 + idleTimeout int + id int +} + +type WebMessageRequest struct { + Ref etf.Ref + Request *http.Request + Response http.ResponseWriter +} + +type webMessageRequest struct { + sync.Mutex + WebMessageRequest + requestState int // 0 - initial, 1 - canceled, 2 - handled +} + +type optsWebHandler struct { + id int + idleTimeout int +} + +type messageWebHandlerIdleCheck struct{} + +func (wh *WebHandler) initHandler(parent Process, handler WebHandlerBehavior, options WebHandlerOptions) (http.Handler, error) { + if options.NumHandlers < 1 { + options.NumHandlers = 1 + } + if options.RequestTimeout < 1 { + options.RequestTimeout = DefaultCallTimeout + } + + if options.IdleTimeout < 0 { + options.IdleTimeout = 0 + } + + if options.RequestQueueLength < 1 { + options.RequestQueueLength = defaultRequestQueueLength + } + + wh.parent = parent + wh.behavior = handler + wh.options = options + c := atomic.AddUint64(&wh.counter, 1) + if c > 1 { + return nil, fmt.Errorf("you can not use the same object more than once") + } + + for i := 0; i < options.NumHandlers; i++ { + p := wh.startHandler(i, options.IdleTimeout) + if p == nil { + return nil, fmt.Errorf("can not initialize handlers") + } + wh.pool = append(wh.pool, &p) + } + return wh, nil +} + +func (wh *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var p Process + + //w.WriteHeader(http.StatusOK) + //return + + mr := webMessageRequestPool.Get().(*webMessageRequest) + mr.Request = r + mr.Response = w + mr.requestState = 0 + + timeout := wh.options.RequestTimeout + if t := r.Header.Get("Request-Timeout"); t != "" { + intT, err := strconv.Atoi(t) + if err == nil && intT > 0 { + timeout = intT + } + } + + l := uint64(wh.options.NumHandlers) + // make round robin using the counter value + c := atomic.AddUint64(&wh.counter, 1) + + // attempts + for a := uint64(0); a < l; a++ { + i := (c + a) % l + + p = *(*Process)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&wh.pool[i])))) + + respawned: + if r.Context().Err() != nil { + // canceled by the client + return + } + _, err := p.DirectWithTimeout(mr, timeout) + switch err { + case nil: + webMessageRequestPool.Put(mr) + return + + case lib.ErrProcessTerminated: + mr.Lock() + if mr.requestState > 0 { + mr.Unlock() + return + } + mr.Unlock() + p = wh.startHandler(int(i), wh.options.IdleTimeout) + atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&wh.pool[i])), unsafe.Pointer(&p)) + goto respawned + + case lib.ErrProcessBusy: + continue + + case lib.ErrTimeout: + mr.Lock() + if mr.requestState == 2 { + // timeout happened during the handling request + mr.Unlock() + webMessageRequestPool.Put(mr) + return + } + mr.requestState = 1 // canceled + mr.Unlock() + w.WriteHeader(http.StatusGatewayTimeout) + return + + default: + lib.Warning("WebHandler %s return error: %s", p.Self(), err) + mr.Lock() + if mr.requestState > 0 { + mr.Unlock() + return + } + mr.Unlock() + + w.WriteHeader(http.StatusInternalServerError) // 500 + return + } + } + + // all handlers are busy + name := reflect.ValueOf(wh.behavior).Elem().Type().Name() + lib.Warning("too many requests for %s", name) + w.WriteHeader(http.StatusServiceUnavailable) // 503 + webMessageRequestPool.Put(mr) +} + +func (wh *WebHandler) startHandler(id int, idleTimeout int) Process { + opts := ProcessOptions{ + Context: wh.parent.Context(), + DirectboxSize: uint16(wh.options.RequestQueueLength), + } + + optsHandler := optsWebHandler{id: id, idleTimeout: idleTimeout} + p, err := wh.parent.Spawn("", opts, wh.behavior, optsHandler) + if err != nil { + lib.Warning("can not start WebHandler: %s", err) + return nil + } + return p +} + +func (wh *WebHandler) Init(process *ServerProcess, args ...etf.Term) error { + behavior, ok := process.Behavior().(WebHandlerBehavior) + if !ok { + return fmt.Errorf("Web: not a WebHandlerBehavior") + } + handlerProcess := &WebHandlerProcess{ + ServerProcess: *process, + behavior: behavior, + } + if len(args) == 0 { + return fmt.Errorf("Web: can not start with no args") + } + + if a, ok := args[0].(optsWebHandler); ok { + handlerProcess.idleTimeout = a.idleTimeout + handlerProcess.id = a.id + } else { + return fmt.Errorf("Web: wrong args for the WebHandler") + } + + // do not inherit parent State + handlerProcess.State = nil + process.State = handlerProcess + + if handlerProcess.idleTimeout > 0 { + process.CastAfter(process.Self(), messageWebHandlerIdleCheck{}, 5*time.Second) + } + + return nil +} + +func (wh *WebHandler) HandleCall(process *ServerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + whp := process.State.(*WebHandlerProcess) + return whp.behavior.HandleWebHandlerCall(whp, from, message) +} + +func (wh *WebHandler) HandleCast(process *ServerProcess, message etf.Term) ServerStatus { + whp := process.State.(*WebHandlerProcess) + switch message.(type) { + case messageWebHandlerIdleCheck: + if time.Now().Unix()-whp.lastRequest > int64(whp.idleTimeout) { + return ServerStatusStop + } + process.CastAfter(process.Self(), messageWebHandlerIdleCheck{}, 5*time.Second) + + default: + return whp.behavior.HandleWebHandlerCast(whp, message) + } + return ServerStatusOK +} + +func (wh *WebHandler) HandleInfo(process *ServerProcess, message etf.Term) ServerStatus { + whp := process.State.(*WebHandlerProcess) + return whp.behavior.HandleWebHandlerInfo(whp, message) +} + +func (wh *WebHandler) HandleDirect(process *ServerProcess, ref etf.Ref, message interface{}) (interface{}, DirectStatus) { + whp := process.State.(*WebHandlerProcess) + switch m := message.(type) { + case *webMessageRequest: + whp.lastRequest = time.Now().Unix() + whp.counter++ + m.Lock() + defer m.Unlock() + if m.requestState != 0 || m.Request.Context().Err() != nil { // canceled + return nil, DirectStatusOK + } + m.requestState = 2 // handled + m.Ref = ref + status := whp.behavior.HandleRequest(whp, m.WebMessageRequest) + switch status { + case WebHandlerStatusDone: + return nil, DirectStatusOK + + case WebHandlerStatusWait: + return nil, DirectStatusIgnore + default: + return nil, status + } + } + return nil, DirectStatusOK +} + +func (wh *WebHandler) Terminate(process *ServerProcess, reason string) { + whp := process.State.(*WebHandlerProcess) + whp.behavior.HandleWebHandlerTerminate(whp, reason, whp.counter) +} + +// HandleWebHandlerCall +func (wh *WebHandler) HandleWebHandlerCall(process *WebHandlerProcess, from ServerFrom, message etf.Term) (etf.Term, ServerStatus) { + lib.Warning("HandleWebHandlerCall: unhandled message (from %#v) %#v", from, message) + return etf.Atom("ok"), ServerStatusOK +} + +// HandleWebHandlerCast +func (wh *WebHandler) HandleWebHandlerCast(process *WebHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleWebHandlerCast: unhandled message %#v", message) + return ServerStatusOK +} + +// HandleWebHandlerInfo +func (wh *WebHandler) HandleWebHandlerInfo(process *WebHandlerProcess, message etf.Term) ServerStatus { + lib.Warning("HandleWebHandlerInfo: unhandled message %#v", message) + return ServerStatusOK +} +func (wh *WebHandler) HandleWebHandlerTerminate(process *WebHandlerProcess, reason string, count int64) { + return +} + +// we should disable SetTrapExit for the WebHandlerProcess by overriding it. +func (whp *WebHandlerProcess) SetTrapExit(trap bool) { + lib.Warning("[%s] method 'SetTrapExit' is disabled for WebHandlerProcess", whp.Self()) +} diff --git a/lib/cert.go b/lib/cert.go new file mode 100644 index 00000000..ada1d0fa --- /dev/null +++ b/lib/cert.go @@ -0,0 +1,90 @@ +package lib + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "sync" + "time" +) + +// GenerateSelfSignedCert +func GenerateSelfSignedCert(org string) (tls.Certificate, error) { + var cert = tls.Certificate{} + certPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return cert, err + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return cert, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{org}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + IsCA: true, + + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certBytes, err1 := x509.CreateCertificate(rand.Reader, &template, &template, + &certPrivKey.PublicKey, certPrivKey) + if err1 != nil { + return cert, err1 + } + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + x509Encoded, _ := x509.MarshalECPrivateKey(certPrivKey) + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509Encoded, + }) + + return tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes()) +} + +type CertUpdater struct { + sync.RWMutex + cert *tls.Certificate +} + +func CreateCertUpdater(cert tls.Certificate) *CertUpdater { + return &CertUpdater{ + cert: &cert, + } +} + +func (cu *CertUpdater) Update(cert tls.Certificate) { + cu.Lock() + defer cu.Unlock() + + cu.cert = &cert +} + +func (cu *CertUpdater) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) { + cu.RLock() + defer cu.RUnlock() + return cu.cert, nil + } +} diff --git a/lib/errors.go b/lib/errors.go new file mode 100644 index 00000000..b1c3450a --- /dev/null +++ b/lib/errors.go @@ -0,0 +1,52 @@ +package lib + +import "fmt" + +var ( + ErrAppAlreadyLoaded = fmt.Errorf("application is already loaded") + ErrAppAlreadyStarted = fmt.Errorf("application is already started") + ErrAppUnknown = fmt.Errorf("unknown application name") + ErrAppIsNotRunning = fmt.Errorf("application is not running") + ErrNameUnknown = fmt.Errorf("unknown name") + ErrNameOwner = fmt.Errorf("not an owner") + ErrProcessBusy = fmt.Errorf("process is busy") + ErrProcessMailboxFull = fmt.Errorf("process mailbox is full") + ErrProcessUnknown = fmt.Errorf("unknown process") + ErrProcessContext = fmt.Errorf("not a Process context") + ErrProcessIncarnation = fmt.Errorf("process ID belongs to the previous incarnation") + ErrProcessTerminated = fmt.Errorf("process terminated") + ErrMonitorUnknown = fmt.Errorf("unknown monitor reference") + ErrSenderUnknown = fmt.Errorf("unknown sender") + ErrBehaviorUnknown = fmt.Errorf("unknown behavior") + ErrBehaviorGroupUnknown = fmt.Errorf("unknown behavior group") + ErrAliasUnknown = fmt.Errorf("unknown alias") + ErrAliasOwner = fmt.Errorf("not an owner") + ErrEventMismatch = fmt.Errorf("message type mismatch") + ErrEventUnknown = fmt.Errorf("unknown event type") + ErrEventOwner = fmt.Errorf("not an owner") + ErrEventSelf = fmt.Errorf("monitor events from itself") + ErrNoRoute = fmt.Errorf("no route to node") + ErrTaken = fmt.Errorf("resource is taken") + ErrFragmented = fmt.Errorf("fragmented data") + ErrReferenceUnknown = fmt.Errorf("unknown reference") + + ErrRouteName = fmt.Errorf("incorrect route name") + + ErrTimeout = fmt.Errorf("timed out") + ErrUnsupported = fmt.Errorf("not supported") + ErrUnknown = fmt.Errorf("unknown") + ErrPeerUnsupported = fmt.Errorf("peer does not support this feature") + + ErrUnsupportedRequest = fmt.Errorf("unsupported request") + ErrServerTerminated = fmt.Errorf("server terminated") + + ErrProxyUnknownRequest = fmt.Errorf("unknown proxy request") + ErrProxyTransitDisabled = fmt.Errorf("proxy feature disabled") + ErrProxyNoRoute = fmt.Errorf("no proxy route to node") + ErrProxyConnect = fmt.Errorf("can't establish proxy connection") + ErrProxyHopExceeded = fmt.Errorf("proxy hop is exceeded") + ErrProxyLoopDetected = fmt.Errorf("proxy loop detected") + ErrProxyPathTooLong = fmt.Errorf("proxy path too long") + ErrProxySessionUnknown = fmt.Errorf("unknown session id") + ErrProxySessionDuplicate = fmt.Errorf("session is already exist") +) diff --git a/lib/mpsc.go b/lib/mpsc.go new file mode 100644 index 00000000..253d4e5f --- /dev/null +++ b/lib/mpsc.go @@ -0,0 +1,162 @@ +// this is a lock-free implementation of MPSC queue (Multiple Producers Single Consumer) + +package lib + +import ( + "math" + "sync/atomic" + "unsafe" +) + +type queueMPSC struct { + head *itemMPSC + tail *itemMPSC +} + +type queueLimitMPSC struct { + head *itemMPSC + tail *itemMPSC + length int64 + limit int64 +} + +type QueueMPSC interface { + Push(value interface{}) bool + Pop() (interface{}, bool) + Item() ItemMPSC + // Len returns the number of items in the queue + Len() int64 +} + +func NewQueueMPSC() QueueMPSC { + emptyItem := &itemMPSC{} + return &queueMPSC{ + head: emptyItem, + tail: emptyItem, + } +} + +func NewQueueLimitMPSC(limit int64) QueueMPSC { + if limit < 1 { + limit = math.MaxInt64 + } + emptyItem := &itemMPSC{} + return &queueLimitMPSC{ + limit: limit, + head: emptyItem, + tail: emptyItem, + } +} + +type ItemMPSC interface { + Next() ItemMPSC + Value() interface{} + Clear() +} + +type itemMPSC struct { + value interface{} + next *itemMPSC +} + +// Push place the given value in the queue head (FIFO). Returns always true +func (q *queueMPSC) Push(value interface{}) bool { + i := &itemMPSC{ + value: value, + } + old_head := (*itemMPSC)(atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&q.head)), unsafe.Pointer(i))) + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&old_head.next)), unsafe.Pointer(i)) + return true +} + +// Push place the given value in the queue head (FIFO). Returns false if exceeded the limit +func (q *queueLimitMPSC) Push(value interface{}) bool { + if q.Len()+1 > q.limit { + return false + } + atomic.AddInt64(&q.length, 1) + i := &itemMPSC{ + value: value, + } + old_head := (*itemMPSC)(atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&q.head)), unsafe.Pointer(i))) + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&old_head.next)), unsafe.Pointer(i)) + return true +} + +// Pop takes the item from the queue tail. Returns false if the queue is empty. Can be used in a single consumer (goroutine) only. +func (q *queueMPSC) Pop() (interface{}, bool) { + tail_next := (*itemMPSC)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail.next)))) + if tail_next == nil { + return nil, false + } + + value := tail_next.value + tail_next.value = nil // let the GC free this item + q.tail = tail_next + return value, true +} + +// Pop takes the item from the queue tail. Returns false if the queue is empty. Can be used in a single consumer (goroutine) only. +func (q *queueLimitMPSC) Pop() (interface{}, bool) { + tail_next := (*itemMPSC)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail.next)))) + if tail_next == nil { + return nil, false + } + + value := tail_next.value + tail_next.value = nil // let the GC free this item + q.tail = tail_next + atomic.AddInt64(&q.length, -1) + return value, true +} + +// Len returns -1 for the queue with no limit +func (q *queueMPSC) Len() int64 { + return -1 +} + +// Len returns queue length +func (q *queueLimitMPSC) Len() int64 { + return atomic.LoadInt64(&q.length) +} + +// Item returns the tail item of the queue. Returns nil if queue is empty. +func (q *queueMPSC) Item() ItemMPSC { + item := (*itemMPSC)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail.next)))) + if item == nil { + return nil + } + return item +} + +// Item returns the tail item of the queue. Returns nil if queue is empty. +func (q *queueLimitMPSC) Item() ItemMPSC { + item := (*itemMPSC)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail.next)))) + if item == nil { + return nil + } + return item +} + +// +// ItemMPSC interface implementation +// + +// Next provides walking through the queue. Returns nil if the last item is reached. +func (i *itemMPSC) Next() ItemMPSC { + next := (*itemMPSC)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&i.next)))) + if next == nil { + return nil + } + return next +} + +// Value returns stored value of the queue item +func (i *itemMPSC) Value() interface{} { + return i.value +} + +// Clear sets the value to nil. It doesn't remove this item from the queue. Can be used in a signle consumer (goroutine) only. +func (i *itemMPSC) Clear() { + i.value = nil +} diff --git a/lib/mpsc_test.go b/lib/mpsc_test.go new file mode 100644 index 00000000..2a8242e0 --- /dev/null +++ b/lib/mpsc_test.go @@ -0,0 +1,222 @@ +package lib + +import ( + "math/rand" + "runtime" + "strconv" + "sync" + "sync/atomic" + "testing" +) + +func TestMPSCsequential(t *testing.T) { + + type vv struct { + v int64 + } + l := int64(10) + queue := NewQueueLimitMPSC(l) + // append to the queue + for i := int64(0); i < l; i++ { + v := vv{v: i + 100} + if queue.Push(v) == false { + t.Fatal("can't push value into the queue") + } + } + if queue.Len() != l { + t.Fatal("queue length must be 10") + } + + if queue.Push("must be failed") == true { + t.Fatal("must be false: exceeded the limit", queue.Len()) + } + + // walking through the queue + item := queue.Item() + for i := int64(0); i < l; i++ { + v, ok := item.Value().(vv) + if ok == false || v.v != i+100 { + t.Fatal("incorrect value. expected", i+100, "got", v) + } + + item = item.Next() + + } + if item != nil { + t.Fatal("there is something else in the queue", item.Value()) + } + + // popping from the queue + for i := int64(0); i < l; i++ { + value, ok := queue.Pop() + if ok == false { + t.Fatal("there must be value") + } + v, ok := value.(vv) + if ok == false || v.v != i+100 { + t.Fatal("incorrect value. expected", i+100, "got", v) + } + } + + // must be empty + if queue.Len() != 0 { + t.Fatal("queue length must be 0") + } + + // check Clear method + if ok := queue.Push(vv{v: 100}); ok == false { + t.Fatal("must be true here") + } + + item = queue.Item() + if item == nil { + t.Fatal("item is nil") + } + item.Clear() + value, ok := queue.Pop() + if ok == false { + t.Fatal("must be true here") + } + if value != nil { + t.Fatal("must be nil here") + } +} + +func TestMPSCparallel(t *testing.T) { + + type vv struct { + v int64 + } + l := int64(100000) + queue := NewQueueLimitMPSC(l) + sum := int64(0) + // append to the queue + var wg sync.WaitGroup + for i := int64(0); i < l; i++ { + v := vv{v: i + 100} + sum += v.v + wg.Add(1) + go func(v vv) { + if queue.Push(v) == false { + t.Fatal("can't push value into the queue") + } + wg.Done() + }(v) + } + wg.Wait() + if x := queue.Len(); x != l { + t.Fatal("queue length must be", l, "have", x) + } + + if queue.Push("must be failed") == true { + t.Fatal("must be false: exceeded the limit", queue.Len()) + } + + // walking through the queue + item := queue.Item() + sum1 := int64(0) + for i := int64(0); i < l; i++ { + v, ok := item.Value().(vv) + sum1 += v.v + if ok == false { + t.Fatal("incorrect value. got", v) + } + + item = item.Next() + + } + if item != nil { + t.Fatal("there is something else in the queue", item.Value()) + } + if sum != sum1 { + t.Fatal("wrong value. exp", sum, "got", sum1) + } + + sum1 = 0 + // popping from the queue + for i := int64(0); i < l; i++ { + value, ok := queue.Pop() + if ok == false { + t.Fatal("there must be value") + } + v, ok := value.(vv) + sum1 += v.v + if ok == false { + t.Fatal("incorrect value. got", v) + } + } + + // must be empty + if queue.Len() != 0 { + t.Fatal("queue length must be 0") + } + if sum != sum1 { + t.Fatal("wrong value. exp", sum, "got", sum1) + } +} + +type chanQueue struct { + q chan interface{} +} + +type testQueue interface { + Push(v interface{}) bool + Pop() (interface{}, bool) +} + +func newChanQueue() *chanQueue { + chq := &chanQueue{ + q: make(chan interface{}, 100000000), + } + return chq +} + +// Enqueue puts the given value v at the tail of the queue. +func (cq *chanQueue) Push(v interface{}) bool { + select { + case cq.q <- v: + return true + default: + panic("channel is full") + } +} + +func (cq *chanQueue) Pop() (interface{}, bool) { + v := <-cq.q + return v, true +} +func BenchmarkMPSC(b *testing.B) { + queues := map[string]testQueue{ + "Chan queue ": newChanQueue(), + "MPSC queue ": NewQueueMPSC(), + "MPSC with limit queue": NewQueueLimitMPSC(0), + } + + length := 1 << 12 + inputs := make([]int, length) + for i := 0; i < length; i++ { + inputs = append(inputs, rand.Int()) + } + + for _, cpus := range []int{4, 32, 1024} { + runtime.GOMAXPROCS(cpus) + for name, q := range queues { + b.Run(name+"#"+strconv.Itoa(cpus), func(b *testing.B) { + b.ResetTimer() + + var c int64 + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + i := int(atomic.AddInt64(&c, 1)-1) % length + v := inputs[i] + if v >= 0 { + q.Push(v) + } else { + q.Pop() + } + } + }) + }) + } + } +} diff --git a/lib/netreadwriter.go b/lib/netreadwriter.go new file mode 100644 index 00000000..9f157ee1 --- /dev/null +++ b/lib/netreadwriter.go @@ -0,0 +1,21 @@ +package lib + +import ( + "io" + "time" +) + +type NetReadWriter interface { + NetReader + NetWriter +} + +type NetReader interface { + io.Reader + SetReadDeadline(t time.Time) error +} + +type NetWriter interface { + io.Writer + SetWriteDeadline(t time.Time) error +} diff --git a/lib/osdep/bsd.go b/lib/osdep/bsd.go index dc8c2278..9efc38c1 100644 --- a/lib/osdep/bsd.go +++ b/lib/osdep/bsd.go @@ -12,8 +12,8 @@ func ResourceUsage() (int64, int64) { var usage syscall.Rusage var utime, stime int64 if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err == nil { - utime = usage.Utime.Sec*1000000000 + usage.Utime.Nano() - stime = usage.Stime.Sec*1000000000 + usage.Stime.Nano() + utime = int64(usage.Utime.Sec)*1000000000 + usage.Utime.Nano() + stime = int64(usage.Stime.Sec)*1000000000 + usage.Stime.Nano() } return utime, stime } diff --git a/lib/osdep/darwin.go b/lib/osdep/darwin.go index 30742c70..b31028c6 100644 --- a/lib/osdep/darwin.go +++ b/lib/osdep/darwin.go @@ -12,8 +12,8 @@ func ResourceUsage() (int64, int64) { var usage syscall.Rusage var utime, stime int64 if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err == nil { - utime = usage.Utime.Sec*1000000000 + usage.Utime.Nano() - stime = usage.Stime.Sec*1000000000 + usage.Stime.Nano() + utime = int64(usage.Utime.Sec)*1000000000 + usage.Utime.Nano() + stime = int64(usage.Stime.Sec)*1000000000 + usage.Stime.Nano() } return utime, stime } diff --git a/lib/osdep/linux.go b/lib/osdep/linux.go index 1c2e1cdc..fcb22d59 100644 --- a/lib/osdep/linux.go +++ b/lib/osdep/linux.go @@ -12,8 +12,8 @@ func ResourceUsage() (int64, int64) { var usage syscall.Rusage var utime, stime int64 if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err == nil { - utime = usage.Utime.Sec*1000000000 + usage.Utime.Nano() - stime = usage.Stime.Sec*1000000000 + usage.Stime.Nano() + utime = int64(usage.Utime.Sec)*1000000000 + usage.Utime.Nano() + stime = int64(usage.Stime.Sec)*1000000000 + usage.Stime.Nano() } return utime, stime } diff --git a/lib/tools.go b/lib/tools.go index 06a3f2d9..0396decb 100644 --- a/lib/tools.go +++ b/lib/tools.go @@ -5,8 +5,10 @@ import ( "encoding/hex" "flag" "fmt" + "hash/crc32" "io" "log" + "math" "sync" "time" ) @@ -40,6 +42,8 @@ var ( } ErrTooLarge = fmt.Errorf("Too large") + + CRC32Q = crc32.MakeTable(0xD5828281) ) func init() { @@ -163,7 +167,7 @@ func (b *Buffer) ReadDataFrom(r io.Reader, limit int) (int, error) { capB := cap(b.B) lenB := len(b.B) if limit == 0 { - limit = 4294967000 + limit = math.MaxInt } // if buffer becomes too large if lenB > limit { diff --git a/node/core.go b/node/core.go index 239ae934..adbcbc08 100644 --- a/node/core.go +++ b/node/core.go @@ -15,8 +15,11 @@ import ( "github.com/ergo-services/ergo/lib" ) -const ( - startPID = 1000 +var ( + startPID = uint64(1000) + startUniqID = uint64(time.Now().UnixNano()) + + corePID = etf.Pid{} ) type core struct { @@ -29,7 +32,6 @@ type core struct { env map[gen.EnvKey]interface{} mutexEnv sync.RWMutex - tls TLS compression Compression nextPID uint64 @@ -75,6 +77,18 @@ type coreInternal interface { coreWait() coreWaitWithTimeout(d time.Duration) error + + monitorStats() internalMonitorStats + networkStats() internalNetworkStats + coreStats() internalCoreStats +} + +type internalCoreStats struct { + totalProcesses uint64 + totalReferences uint64 + processes int + aliases int + names int } type coreRouterInternal interface { @@ -87,6 +101,7 @@ type coreRouterInternal interface { processByPid(pid etf.Pid) *process getConnection(nodename string) (ConnectionInterface, error) + sendEvent(pid etf.Pid, event gen.Event, message gen.EventMessage) error } // transit proxy session @@ -110,10 +125,9 @@ func newCore(ctx context.Context, nodename string, cookie string, options Option options.Compression.Threshold = DefaultCompressionThreshold } c := &core{ - ctx: ctx, env: options.Env, nextPID: startPID, - uniqID: uint64(time.Now().UnixNano()), + uniqID: startUniqID, // keep node to get the process to access to the node's methods nodename: nodename, compression: options.Compression, @@ -124,9 +138,15 @@ func newCore(ctx context.Context, nodename string, cookie string, options Option behaviors: make(map[string]map[string]gen.RegisteredBehavior), } + corePID = etf.Pid{ + Node: etf.Atom(c.nodename), + ID: 1, + Creation: c.creation, + } + corectx, corestop := context.WithCancel(ctx) c.stop = corestop - c.ctx = corectx + c.ctx = context.WithValue(corectx, c, c) c.monitorInternal = newMonitor(nodename, coreRouterInternal(c)) network, err := newNetwork(c.ctx, nodename, cookie, options, coreRouterInternal(c)) @@ -136,6 +156,8 @@ func newCore(ctx context.Context, nodename string, cookie string, options Option } c.networkInternal = network + c.registerEvent(corePID, EventNetwork, []gen.EventMessage{MessageEventNetwork{}}) + return c, nil } @@ -164,7 +186,7 @@ func (c *core) coreWaitWithTimeout(d time.Duration) error { select { case <-timer.C: - return ErrTimeout + return lib.ErrTimeout case <-c.ctx.Done(): return nil } @@ -213,7 +235,7 @@ func (c *core) newAlias(p *process) (etf.Alias, error) { _, exist := c.processes[p.self.ID] c.mutexProcesses.RUnlock() if !exist { - return alias, ErrProcessUnknown + return alias, lib.ErrProcessUnknown } alias = etf.Alias(c.MakeRef()) @@ -237,7 +259,7 @@ func (c *core) deleteAlias(owner *process, alias etf.Alias) error { c.mutexAliases.Unlock() if alias_exist == false { - return ErrAliasUnknown + return lib.ErrAliasUnknown } c.mutexProcesses.RLock() @@ -245,10 +267,10 @@ func (c *core) deleteAlias(owner *process, alias etf.Alias) error { c.mutexProcesses.RUnlock() if process_exist == false { - return ErrProcessUnknown + return lib.ErrProcessUnknown } if p.self != owner.self { - return ErrAliasOwner + return lib.ErrAliasOwner } p.Lock() @@ -274,19 +296,29 @@ func (c *core) deleteAlias(owner *process, alias etf.Alias) error { c.mutexAliases.Unlock() lib.Warning("Bug: Process lost its alias. Please, report this issue") - return ErrAliasUnknown + return lib.ErrAliasUnknown } func (c *core) newProcess(name string, behavior gen.ProcessBehavior, opts processOptions) (*process, error) { + var processContext context.Context + var kill context.CancelFunc mailboxSize := DefaultProcessMailboxSize if opts.MailboxSize > 0 { mailboxSize = int(opts.MailboxSize) } + directboxSize := DefaultProcessDirectboxSize + if opts.DirectboxSize > 0 { + directboxSize = int(opts.DirectboxSize) + } - processContext, kill := context.WithCancel(c.ctx) if opts.Context != nil { - processContext, _ = context.WithCancel(opts.Context) + if opts.Context.Value(c) != c { + return nil, lib.ErrProcessContext + } + processContext, kill = context.WithCancel(opts.Context) + } else { + processContext, kill = context.WithCancel(c.ctx) } pid := c.newPID() @@ -318,12 +350,12 @@ func (c *core) newProcess(name string, behavior gen.ProcessBehavior, opts proces mailBox: make(chan gen.ProcessMailboxMessage, mailboxSize), gracefulExit: make(chan gen.ProcessGracefulExitRequest, mailboxSize), - direct: make(chan gen.ProcessDirectMessage), + direct: make(chan gen.ProcessDirectMessage, directboxSize), context: processContext, kill: kill, - reply: make(map[etf.Ref]chan etf.Term), + reply: make(map[etf.Ref]chan syncReplyMessage), fallback: opts.Fallback, } @@ -331,7 +363,7 @@ func (c *core) newProcess(name string, behavior gen.ProcessBehavior, opts proces lib.Log("[%s] EXIT from %s to %s with reason: %s", c.nodename, from, pid, reason) if processContext.Err() != nil { // process is already died - return ErrProcessUnknown + return lib.ErrProcessUnknown } ex := gen.ProcessGracefulExitRequest{ @@ -345,7 +377,7 @@ func (c *core) newProcess(name string, behavior gen.ProcessBehavior, opts proces select { case process.gracefulExit <- ex: default: - return ErrProcessBusy + return lib.ErrProcessBusy } // let the process decide whether to stop itself, otherwise its going to be killed @@ -361,7 +393,7 @@ func (c *core) newProcess(name string, behavior gen.ProcessBehavior, opts proces if _, exist := c.names[name]; exist { c.mutexNames.Unlock() process.kill() // cancel context - return nil, ErrTaken + return nil, lib.ErrTaken } c.names[name] = process.self c.mutexNames.Unlock() @@ -425,6 +457,7 @@ func (c *core) spawn(name string, opts processOptions, behavior gen.ProcessBehav lib.Warning("initialization process failed %s[%q] %#v at %s[%s:%d]", process.self, name, rcv, runtime.FuncForPC(pc).Name(), fn, line) c.deleteProcess(process.self) + process.kill() err = fmt.Errorf("panic") } }() @@ -502,7 +535,7 @@ func (c *core) registerName(name string, pid etf.Pid) error { defer c.mutexNames.Unlock() if _, ok := c.names[name]; ok { // already registered - return ErrTaken + return lib.ErrTaken } c.names[name] = pid return nil @@ -516,7 +549,7 @@ func (c *core) unregisterName(name string) error { delete(c.names, name) return nil } - return ErrNameUnknown + return lib.ErrNameUnknown } // ListEnv @@ -569,7 +602,7 @@ func (c *core) RegisterBehavior(group, name string, behavior gen.ProcessBehavior _, exist = groupBehaviors[name] if exist { - return ErrTaken + return lib.ErrTaken } rb := gen.RegisteredBehavior{ @@ -591,12 +624,12 @@ func (c *core) RegisteredBehavior(group, name string) (gen.RegisteredBehavior, e groupBehaviors, exist = c.behaviors[group] if !exist { - return rb, ErrBehaviorGroupUnknown + return rb, lib.ErrBehaviorGroupUnknown } rb, exist = groupBehaviors[name] if !exist { - return rb, ErrBehaviorUnknown + return rb, lib.ErrBehaviorUnknown } return rb, nil } @@ -632,7 +665,7 @@ func (c *core) UnregisterBehavior(group, name string) error { groupBehaviors, exist = c.behaviors[group] if !exist { - return ErrBehaviorUnknown + return lib.ErrBehaviorUnknown } delete(groupBehaviors, name) @@ -716,7 +749,7 @@ func (c *core) RouteSend(from etf.Pid, to etf.Pid, message etf.Term) error { if to.Creation != c.creation { // message is addressed to the previous incarnation of this PID lib.Warning("message from %s is addressed to the previous incarnation of this PID %s", from, to) - return ErrProcessIncarnation + return lib.ErrProcessIncarnation } // local route c.mutexProcesses.RLock() @@ -724,7 +757,7 @@ func (c *core) RouteSend(from etf.Pid, to etf.Pid, message etf.Term) error { c.mutexProcesses.RUnlock() if !exist { lib.Log("[%s] CORE route message by pid (local) %s failed. Unknown process", c.nodename, to) - return ErrProcessUnknown + return lib.ErrProcessUnknown } lib.Log("[%s] CORE route message by pid (local) %s", c.nodename, to) select { @@ -734,10 +767,8 @@ func (c *core) RouteSend(from etf.Pid, to etf.Pid, message etf.Term) error { pid, found := c.names[p.fallback.Name] c.mutexNames.RUnlock() if found == false { - //lib.Warning("mailbox of %s[%q] is full. dropped message from %s", p.self, p.name, from) - //FIXME - lib.Warning("mailbox of %s[%q] is full. dropped message from %s %#v", p.self, p.name, from, message) - return ErrProcessBusy + lib.Warning("mailbox of %s[%q] is full. dropped message from %s", p.self, p.name, from) + return lib.ErrProcessMailboxFull } fbm := gen.MessageFallback{ Process: p.self, @@ -751,7 +782,7 @@ func (c *core) RouteSend(from etf.Pid, to etf.Pid, message etf.Term) error { // do not allow to send from the alien node. if string(from.Node) != c.nodename { - return ErrSenderUnknown + return lib.ErrSenderUnknown } // sending to remote node @@ -760,7 +791,7 @@ func (c *core) RouteSend(from etf.Pid, to etf.Pid, message etf.Term) error { c.mutexProcesses.RUnlock() if !exist { lib.Log("[%s] CORE route message by pid (remote) %s failed. Unknown sender", c.nodename, to) - return ErrSenderUnknown + return lib.ErrSenderUnknown } connection, err := c.getConnection(string(to.Node)) if err != nil { @@ -780,7 +811,7 @@ func (c *core) RouteSendReg(from etf.Pid, to gen.ProcessID, message etf.Term) er c.mutexNames.RUnlock() if !ok { lib.Log("[%s] CORE route message by gen.ProcessID (local) %s failed. Unknown process", c.nodename, to) - return ErrProcessUnknown + return lib.ErrProcessUnknown } lib.Log("[%s] CORE route message by gen.ProcessID (local) %s", c.nodename, to) return c.RouteSend(from, pid, message) @@ -788,7 +819,7 @@ func (c *core) RouteSendReg(from etf.Pid, to gen.ProcessID, message etf.Term) er // do not allow to send from the alien node. if string(from.Node) != c.nodename { - return ErrSenderUnknown + return lib.ErrSenderUnknown } // send to remote node @@ -797,7 +828,7 @@ func (c *core) RouteSendReg(from etf.Pid, to gen.ProcessID, message etf.Term) er c.mutexProcesses.RUnlock() if !exist { lib.Log("[%s] CORE route message by gen.ProcessID (remote) %s failed. Unknown sender", c.nodename, to) - return ErrSenderUnknown + return lib.ErrSenderUnknown } connection, err := c.getConnection(string(to.Node)) if err != nil { @@ -818,7 +849,7 @@ func (c *core) RouteSendAlias(from etf.Pid, to etf.Alias, message etf.Term) erro c.mutexAliases.RUnlock() if !ok { lib.Log("[%s] CORE route message by alias (local) %s failed. Unknown process", c.nodename, to) - return ErrProcessUnknown + return lib.ErrProcessUnknown } lib.Log("[%s] CORE route message by alias (local) %s", c.nodename, to) return c.RouteSend(from, process.self, message) @@ -826,7 +857,7 @@ func (c *core) RouteSendAlias(from etf.Pid, to etf.Alias, message etf.Term) erro // do not allow to send from the alien node. Proxy request must be used. if string(from.Node) != c.nodename { - return ErrSenderUnknown + return lib.ErrSenderUnknown } // send to remote node @@ -835,7 +866,7 @@ func (c *core) RouteSendAlias(from etf.Pid, to etf.Alias, message etf.Term) erro c.mutexProcesses.RUnlock() if !exist { lib.Log("[%s] CORE route message by alias (remote) %s failed. Unknown sender", c.nodename, to) - return ErrSenderUnknown + return lib.ErrSenderUnknown } connection, err := c.getConnection(string(to.Node)) if err != nil { @@ -885,9 +916,9 @@ func (c *core) RouteSpawnReply(to etf.Pid, ref etf.Ref, result etf.Term) error { process := c.processByPid(to) if process == nil { // seems process terminated - return ErrProcessTerminated + return lib.ErrProcessTerminated } - process.PutSyncReply(ref, result) + process.PutSyncReply(ref, result, nil) return nil } @@ -900,3 +931,22 @@ func (c *core) processByPid(pid etf.Pid) *process { // unknown process return nil } + +func (c *core) coreStats() internalCoreStats { + stats := internalCoreStats{} + stats.totalProcesses = atomic.LoadUint64(&c.nextPID) - startPID + stats.totalReferences = atomic.LoadUint64(&c.uniqID) - startUniqID + + c.mutexProcesses.RLock() + stats.processes = len(c.processes) + c.mutexProcesses.RUnlock() + + c.mutexAliases.RLock() + stats.aliases = len(c.aliases) + c.mutexAliases.RUnlock() + + c.mutexNames.RLock() + stats.names = len(c.names) + c.mutexNames.RUnlock() + return stats +} diff --git a/node/monitor.go b/node/monitor.go index cf283eff..cac9d718 100644 --- a/node/monitor.go +++ b/node/monitor.go @@ -4,6 +4,7 @@ package node import ( "fmt" + "reflect" "sync" "github.com/ergo-services/ergo/etf" @@ -16,6 +17,12 @@ type monitorItem struct { ref etf.Ref } +type eventItem struct { + owner etf.Pid + messageTypes map[string]bool + monitors []etf.Pid +} + type monitorInternal interface { // RouteLink RouteLink(pidA etf.Pid, pidB etf.Pid) error @@ -42,12 +49,27 @@ type monitorInternal interface { monitorNode(by etf.Pid, node string, ref etf.Ref) demonitorNode(ref etf.Ref) bool + registerEvent(by etf.Pid, event gen.Event, messages []gen.EventMessage) error + unregisterEvent(by etf.Pid, event gen.Event) error + monitorEvent(by etf.Pid, event gen.Event) error + demonitorEvent(by etf.Pid, event gen.Event) error + sendEvent(by etf.Pid, event gen.Event, message gen.EventMessage) error + handleTerminated(terminated etf.Pid, name, reason string) processLinks(process etf.Pid) []etf.Pid processMonitors(process etf.Pid) []etf.Pid processMonitorsByName(process etf.Pid) []gen.ProcessID processMonitoredBy(process etf.Pid) []etf.Pid + + monitorStats() internalMonitorStats +} + +type internalMonitorStats struct { + monitorsByPid int + monitorsByName int + monitorsNodes int + links int } type monitor struct { @@ -69,6 +91,11 @@ type monitor struct { ref2node map[etf.Ref]string mutexNodes sync.RWMutex + // monitors of events + events map[gen.Event]eventItem + pid2events map[etf.Pid][]gen.Event + mutexEvents sync.RWMutex + nodename string router coreRouterInternal } @@ -84,6 +111,9 @@ func newMonitor(nodename string, router coreRouterInternal) monitorInternal { ref2name: make(map[etf.Ref]gen.ProcessID), ref2node: make(map[etf.Ref]string), + events: make(map[gen.Event]eventItem), + pid2events: make(map[etf.Pid][]gen.Event), + nodename: nodename, router: router, } @@ -266,6 +296,7 @@ func (m *monitor) handleTerminated(terminated etf.Pid, name string, reason strin } } m.mutexNames.Unlock() + // check whether we have monitorItem on this process by Pid (terminated) m.mutexProcesses.Lock() if items, ok := m.processes[terminated]; ok { @@ -310,6 +341,58 @@ func (m *monitor) handleTerminated(terminated etf.Pid, name string, reason strin } m.mutexLinks.Unlock() + // check for event owning and monitoring + m.mutexEvents.Lock() + events, exist := m.pid2events[terminated] + if exist == false { + // this process hasn't been involved in any events + m.mutexEvents.Unlock() + return + } + + for _, e := range events { + item := m.events[e] + if item.owner == terminated { + message := gen.MessageEventDown{ + Event: e, + Reason: reason, + } + for _, pid := range item.monitors { + pidevents := m.pid2events[pid] + removed := 0 + for i := range pidevents { + if pidevents[i] != e { + continue + } + m.router.RouteSend(etf.Pid{}, pid, message) + pidevents[i] = pidevents[removed] + removed++ + } + pidevents = pidevents[removed:] + if len(pidevents) == 0 { + delete(m.pid2events, pid) + } else { + m.pid2events[pid] = pidevents + } + } + delete(m.events, e) + continue + } + + removed := 0 + for i := range item.monitors { + if item.monitors[i] != terminated { + continue + } + item.monitors[i] = item.monitors[removed] + removed++ + } + item.monitors = item.monitors[removed:] + m.events[e] = item + } + + delete(m.pid2events, terminated) + m.mutexEvents.Unlock() } func (m *monitor) processLinks(process etf.Pid) []etf.Pid { @@ -435,7 +518,7 @@ func (m *monitor) RouteLink(pidA etf.Pid, pidB etf.Pid) error { // otherwise send 'EXIT' message with 'noproc' as a reason if p := m.router.processByPid(pidB); p == nil { m.sendExit(pidA, pidB, "noproc") - return ErrProcessUnknown + return lib.ErrProcessUnknown } m.mutexLinks.Lock() m.links[pidA] = append(linksA, pidB) @@ -573,9 +656,9 @@ func (m *monitor) RouteMonitor(by etf.Pid, pid etf.Pid, ref etf.Ref) error { if err := connection.Monitor(by, pid, ref); err != nil { switch err { - case ErrPeerUnsupported: + case lib.ErrPeerUnsupported: m.sendMonitorExit(by, pid, "unsupported", ref) - case ErrProcessIncarnation: + case lib.ErrProcessIncarnation: m.sendMonitorExit(by, pid, "incarnation", ref) default: m.sendMonitorExit(by, pid, "noconnection", ref) @@ -611,7 +694,7 @@ func (m *monitor) RouteMonitorReg(by etf.Pid, process gen.ProcessID, ref etf.Ref } if err := connection.MonitorReg(by, process, ref); err != nil { - if err == ErrPeerUnsupported { + if err == lib.ErrPeerUnsupported { m.sendMonitorExitReg(by, process, "unsupported", ref) } else { m.sendMonitorExitReg(by, process, "noconnection", ref) @@ -645,7 +728,7 @@ func (m *monitor) RouteDemonitor(by etf.Pid, ref etf.Ref) error { processID, knownRefByName := m.ref2name[ref] if knownRefByName == false { // unknown monitor reference - return ErrMonitorUnknown + return lib.ErrMonitorUnknown } items := m.names[processID] @@ -850,5 +933,178 @@ func (m *monitor) sendExit(to etf.Pid, terminated etf.Pid, reason string) error p.exit(terminated, reason) return nil } - return ErrProcessUnknown + return lib.ErrProcessUnknown +} + +func (m *monitor) registerEvent(by etf.Pid, event gen.Event, messages []gen.EventMessage) error { + m.mutexEvents.Lock() + defer m.mutexEvents.Unlock() + if _, taken := m.events[event]; taken { + return lib.ErrTaken + } + events, _ := m.pid2events[by] + events = append(events, event) + m.pid2events[by] = events + + mt := make(map[string]bool) + for _, m := range messages { + t := reflect.TypeOf(m) + st := t.PkgPath() + "/" + t.Name() + mt[st] = true + } + item := eventItem{ + owner: by, + messageTypes: mt, + } + m.events[event] = item + return nil +} + +func (m *monitor) unregisterEvent(by etf.Pid, event gen.Event) error { + m.mutexEvents.Lock() + defer m.mutexEvents.Unlock() + + item, exist := m.events[event] + if exist == false { + return lib.ErrEventUnknown + } + if item.owner != by { + return lib.ErrEventOwner + } + message := gen.MessageEventDown{ + Event: event, + Reason: "unregistered", + } + + monitors := append(item.monitors, by) + for _, pid := range monitors { + events, _ := m.pid2events[pid] + removed := 0 + for i := range events { + if events[i] != event { + continue + } + if pid != by { + m.router.RouteSend(etf.Pid{}, pid, message) + } + events[i] = events[removed] + removed++ + } + events = events[removed:] + + if len(events) == 0 { + delete(m.pid2events, pid) + } else { + m.pid2events[pid] = events + } + + } + + delete(m.events, event) + return nil +} + +func (m *monitor) monitorEvent(by etf.Pid, event gen.Event) error { + m.mutexEvents.Lock() + defer m.mutexEvents.Unlock() + + item, exist := m.events[event] + if exist == false { + return lib.ErrEventUnknown + } + if item.owner == by { + return lib.ErrEventSelf + } + item.monitors = append(item.monitors, by) + m.events[event] = item + + events, exist := m.pid2events[by] + events = append(events, event) + m.pid2events[by] = events + return nil +} + +func (m *monitor) demonitorEvent(by etf.Pid, event gen.Event) error { + m.mutexEvents.Lock() + defer m.mutexEvents.Unlock() + + item, exist := m.events[event] + if exist == false { + return lib.ErrEventUnknown + } + removed := 0 + for i := range item.monitors { + if item.monitors[i] != by { + continue + } + + item.monitors[i] = item.monitors[removed] + removed++ + } + item.monitors = item.monitors[removed:] + m.events[event] = item + + events, _ := m.pid2events[by] + + removed = 0 + for i := range events { + if events[i] != event { + continue + } + events[i] = events[removed] + } + events = events[removed:] + + if len(events) == 0 { + delete(m.pid2events, by) + } else { + m.pid2events[by] = events + } + + return nil +} + +func (m *monitor) sendEvent(by etf.Pid, event gen.Event, message gen.EventMessage) error { + m.mutexEvents.RLock() + defer m.mutexEvents.RUnlock() + + item, exist := m.events[event] + if exist == false { + return lib.ErrEventUnknown + } + if item.owner != by { + return lib.ErrEventOwner + } + + t := reflect.TypeOf(message) + st := t.PkgPath() + "/" + t.Name() + if _, exist := item.messageTypes[st]; exist == false { + return lib.ErrEventMismatch + } + + for _, pid := range item.monitors { + m.router.RouteSend(etf.Pid{}, pid, message) + } + + return nil +} + +func (m *monitor) monitorStats() internalMonitorStats { + stats := internalMonitorStats{} + m.mutexProcesses.RLock() + stats.monitorsByPid = len(m.processes) + m.mutexProcesses.RUnlock() + + m.mutexNames.RLock() + stats.monitorsByName = len(m.names) + m.mutexNames.RUnlock() + + m.mutexNodes.RLock() + stats.monitorsNodes = len(m.nodes) + m.mutexNodes.RUnlock() + + m.mutexLinks.RLock() + stats.links = len(m.links) + m.mutexLinks.RUnlock() + return stats } diff --git a/node/network.go b/node/network.go index ba6aa52d..fb6ea775 100644 --- a/node/network.go +++ b/node/network.go @@ -3,21 +3,18 @@ package node import ( "bytes" "context" - "encoding/pem" - "math/big" + "encoding/binary" + "io" "sync" "time" "crypto/aes" - "crypto/ecdsa" - "crypto/elliptic" "crypto/md5" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" "fmt" "github.com/ergo-services/ergo/etf" @@ -40,17 +37,22 @@ type networkInternal interface { StaticRoute(name string) (Route, bool) // add/remove proxy route - AddProxyRoute(node string, route ProxyRoute) error - RemoveProxyRoute(node string) bool + AddProxyRoute(route ProxyRoute) error + RemoveProxyRoute(name string) bool ProxyRoutes() []ProxyRoute ProxyRoute(name string) (ProxyRoute, bool) Resolve(peername string) (Route, error) + ResolveProxy(peername string) (ProxyRoute, error) + Connect(peername string) error Disconnect(peername string) error Nodes() []string NodesIndirect() []string + // stats + NetworkStats(name string) (NetworkStats, error) + // core router methods RouteProxyConnectRequest(from ConnectionInterface, request ProxyConnectRequest) error RouteProxyConnectReply(from ConnectionInterface, reply ProxyConnectReply) error @@ -60,6 +62,14 @@ type networkInternal interface { getConnection(peername string) (ConnectionInterface, error) stopNetwork() + + networkStats() internalNetworkStats +} + +type internalNetworkStats struct { + transitConnections int + proxyConnections int + connections int } type connectionInternal struct { @@ -72,12 +82,12 @@ type connectionInternal struct { } type network struct { - nodename string - cookie string - ctx context.Context - listener net.Listener + nodename string + cookie string + ctx context.Context + listeners []net.Listener - resolver Resolver + registrar Registrar staticOnly bool staticRoutes map[string]Route staticRoutesMutex sync.RWMutex @@ -96,10 +106,11 @@ type network struct { proxyConnectRequest map[etf.Ref]proxyConnectRequest proxyConnectRequestMutex sync.RWMutex - tls TLS + tls *tls.Config proxy Proxy version Version creation uint32 + flags Flags router coreRouterInternal handshake HandshakeInterface @@ -114,6 +125,7 @@ func newNetwork(ctx context.Context, nodename string, cookie string, options Opt nodename: nodename, cookie: cookie, ctx: ctx, + tls: options.TLS, staticOnly: options.StaticRoutesOnly, staticRoutes: make(map[string]Route), proxyRoutes: make(map[string]ProxyRoute), @@ -123,61 +135,79 @@ func newNetwork(ctx context.Context, nodename string, cookie string, options Opt proxyTransitSessions: make(map[string]proxyTransitSession), proxyConnectRequest: make(map[etf.Ref]proxyConnectRequest), remoteSpawn: make(map[string]gen.ProcessBehavior), + flags: options.Flags, proxy: options.Proxy, - resolver: options.Resolver, + registrar: options.Registrar, handshake: options.Handshake, proto: options.Proto, router: router, creation: options.Creation, } - nn := strings.Split(nodename, "@") - if len(nn) != 2 { - return nil, fmt.Errorf("(EMPD) FQDN for node name is required (example: node@hostname)") + splitNodeHost := strings.Split(nodename, "@") + if len(splitNodeHost) != 2 { + return nil, fmt.Errorf("FQDN for node name is required (example: node@hostname)") } - n.version, _ = options.Env[EnvKeyVersion].(Version) if n.proxy.Flags.Enable == false { n.proxy.Flags = DefaultProxyFlags() } - n.tls = options.TLS - selfSignedCert, err := generateSelfSignedCert(n.version) - if n.tls.Server.Certificate == nil { - n.tls.Server = selfSignedCert - n.tls.SkipVerify = true - } - if n.tls.Client.Certificate == nil { - n.tls.Client = selfSignedCert - } + n.version, _ = options.Env[EnvKeyVersion].(Version) - err = n.handshake.Init(n.nodename, n.creation, options.Flags) - if err != nil { - return nil, err + if len(options.Listeners) == 0 { + return nil, fmt.Errorf("no listeners defined") } + for i, lo := range options.Listeners { + if lo.TLS == nil { + lo.TLS = options.TLS + } + if lo.Handshake == nil { + lo.Handshake = options.Handshake + } + if lo.Proto == nil { + lo.Proto = options.Proto + } + if lo.Flags.Enable == false { + lo.Flags = options.Flags + } + if lo.Cookie == "" { + lo.Cookie = cookie + } - port, err := n.listen(ctx, nn[1], options.ListenBegin, options.ListenEnd) - if err != nil { - return nil, err - } + if err := lo.Handshake.Init(n.nodename, n.creation, lo.Flags); err != nil { + return nil, err + } - resolverOptions := ResolverOptions{ - NodeVersion: n.version, - HandshakeVersion: n.handshake.Version(), - EnableTLS: n.tls.Enable, - EnableProxy: options.Flags.EnableProxy, - EnableCompression: options.Flags.EnableCompression, - } - if err := n.resolver.Register(n.ctx, nodename, port, resolverOptions); err != nil { - return nil, err + if lo.Listen > 0 { + lo.ListenBegin = lo.Listen + lo.ListenEnd = lo.Listen + lib.Log("Node listener[%d] port: %d", i, lo.Listen) + } else { + if lo.ListenBegin == 0 { + lo.ListenBegin = defaultListenBegin + } + if lo.ListenEnd == 0 { + lo.ListenEnd = defaultListenEnd + } + lib.Log("Node listener[%d] port range: %d...%d", i, lo.ListenBegin, lo.ListenEnd) + } + register := i == 0 + listener, err := n.listen(ctx, splitNodeHost[1], lo, register) + if err != nil { + // close all listening sockets + n.stopNetwork() + return nil, err + } + n.listeners = append(n.listeners, listener) } return n, nil } func (n *network) stopNetwork() { - if n.listener != nil { - n.listener.Close() + for _, l := range n.listeners { + l.Close() } n.connectionsMutex.RLock() defer n.connectionsMutex.RUnlock() @@ -234,7 +264,13 @@ func (n *network) AddStaticRoute(node string, host string, port uint16, options _, exist := n.staticRoutes[node] if exist { - return ErrTaken + return lib.ErrTaken + } + + if options.Handshake != nil { + if err := options.Handshake.Init(n.nodename, n.creation, n.flags); err != nil { + return err + } } n.staticRoutes[node] = route @@ -282,13 +318,13 @@ func (n *network) getConnectionDirect(peername string, connect bool) (Connection } if connect == false { - return nil, ErrNoRoute + return nil, lib.ErrNoRoute } connection, err := n.connect(peername) if err != nil { lib.Log("[%s] CORE no route to node %q: %s", n.nodename, peername, err) - return nil, ErrNoRoute + return nil, lib.ErrNoRoute } return connection, nil @@ -298,12 +334,13 @@ func (n *network) getConnectionDirect(peername string, connect bool) (Connection func (n *network) getConnection(peername string) (ConnectionInterface, error) { if peername == n.nodename { // can't connect to itself - return nil, ErrNoRoute + return nil, lib.ErrNoRoute } n.connectionsMutex.RLock() ci, ok := n.connections[peername] n.connectionsMutex.RUnlock() if ok { + lib.Log("[%s] NETWORK found active connection with %s", n.nodename, peername) return ci.connection, nil } @@ -315,7 +352,7 @@ func (n *network) getConnection(peername string) (ConnectionInterface, error) { } if err := n.RouteProxyConnectRequest(nil, request); err != nil { - if err != ErrProxyNoRoute { + if err != lib.ErrProxyNoRoute { return nil, err } @@ -334,13 +371,13 @@ func (n *network) getConnection(peername string) (ConnectionInterface, error) { // Resolve func (n *network) Resolve(node string) (Route, error) { - n.staticRoutesMutex.Lock() - defer n.staticRoutesMutex.Unlock() + n.staticRoutesMutex.RLock() + defer n.staticRoutesMutex.RUnlock() if r, ok := n.staticRoutes[node]; ok { if r.Port == 0 { // use static option for this route - route, err := n.resolver.Resolve(node) + route, err := n.registrar.Resolve(node) route.Options = r.Options return route, err } @@ -348,10 +385,36 @@ func (n *network) Resolve(node string) (Route, error) { } if n.staticOnly { - return Route{}, ErrNoRoute + return Route{}, lib.ErrNoRoute } - return n.resolver.Resolve(node) + return n.registrar.Resolve(node) +} + +// ResolveProxy +func (n *network) ResolveProxy(name string) (ProxyRoute, error) { + n.proxyRoutesMutex.RLock() + defer n.proxyRoutesMutex.RUnlock() + route, found := n.proxyRoutes[name] + if found == false { + sn := strings.Split(name, "@") + if len(sn) != 2 { + return route, lib.ErrUnknown + } + domain := "@" + sn[1] + route, found = n.proxyRoutes[domain] + } + if found == false { + return n.registrar.ResolveProxy(name) + } + if route.Proxy == "" { + r, err := n.registrar.ResolveProxy(name) + if err != nil { + return route, err + } + route.Proxy = r.Proxy + } + return route, nil } // Connect @@ -366,7 +429,7 @@ func (n *network) Disconnect(node string) error { ci, ok := n.connections[node] n.connectionsMutex.RUnlock() if !ok { - return ErrNoRoute + return lib.ErrNoRoute } if ci.conn == nil { @@ -408,39 +471,61 @@ func (n *network) NodesIndirect() []string { } } return list +} +func (n *network) NetworkStats(name string) (NetworkStats, error) { + var stats NetworkStats + n.connectionsMutex.RLock() + ci, found := n.connections[name] + n.connectionsMutex.RUnlock() + + if found == false { + return stats, lib.ErrUnknown + } + + stats = ci.connection.Stats() + return stats, nil } // RouteProxyConnectRequest func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request ProxyConnectRequest) error { - // check if we have proxy route - n.proxyRoutesMutex.RLock() - route, has_route := n.proxyRoutes[request.To] - n.proxyRoutesMutex.RUnlock() - if request.To != n.nodename { - var connection ConnectionInterface var err error + var connection ConnectionInterface + // + // outgoing proxy request + // + + // check if we already have + n.connectionsMutex.RLock() + if ci, exist := n.connections[request.To]; exist { + connection = ci.connection + } + n.connectionsMutex.RUnlock() if from != nil { // // transit request // + if from == connection { + lib.Log("[%s] NETWORK proxy. Error: proxy route points to the connection this request came from", n.nodename) + return lib.ErrProxyLoopDetected + } + lib.Log("[%s] NETWORK transit proxy connection to %q", n.nodename, request.To) - lib.Log("[%s] NETWORK transit proxy connection to %q via %q", n.nodename, request.To, route.Proxy) // proxy feature must be enabled explicitly for the transitional requests if n.proxy.Transit == false { lib.Log("[%s] NETWORK proxy. Proxy feature is disabled on this node", n.nodename) - return ErrProxyTransitDisabled + return lib.ErrProxyTransitDisabled } if request.Hop < 1 { lib.Log("[%s] NETWORK proxy. Error: exceeded hop limit", n.nodename) - return ErrProxyHopExceeded + return lib.ErrProxyHopExceeded } request.Hop-- if len(request.Path) > defaultProxyPathLimit { - return ErrProxyPathTooLong + return lib.ErrProxyPathTooLong } for i := range request.Path { @@ -448,56 +533,64 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro continue } lib.Log("[%s] NETWORK proxy. Error: loop detected in proxy path %#v", n.nodename, request.Path) - return ErrProxyLoopDetected + return lib.ErrProxyLoopDetected } - // try to connect to the next-hop node - if has_route == false { - connection, err = n.getConnectionDirect(request.To, true) - } else { - connection, err = n.getConnectionDirect(route.Proxy, true) - } + if connection == nil { + // check if we have proxy route + route, err_route := n.ResolveProxy(request.To) + if err_route == nil && route.Proxy != n.nodename { + // proxy request goes to the next hop + connection, err = n.getConnectionDirect(route.Proxy, true) + } else { + connection, err = n.getConnectionDirect(request.To, true) + } - if err != nil { - return err + if err != nil { + return err + } } - if from == connection { - lib.Log("[%s] NETWORK proxy. Error: proxy route points to the connection this request came from", n.nodename) - return ErrProxyLoopDetected - } request.Path = append([]string{n.nodename}, request.Path...) - return connection.ProxyConnectRequest(request) + err = connection.ProxyConnectRequest(request) + return err } - if has_route == false { - // if it was invoked from getConnection ('from' == nil) there will - // be attempt to make direct connection using getConnectionDirect - return ErrProxyNoRoute - } + if connection == nil { + route, err_route := n.ResolveProxy(request.To) + if err_route != nil { + // if it was invoked from getConnection ('from' == nil) there will + // be attempt to make direct connection using getConnectionDirect + return lib.ErrProxyNoRoute + } + + // initiating proxy connection + lib.Log("[%s] NETWORK initiate proxy connection to %q via %q", n.nodename, request.To, route.Proxy) + connection, err = n.getConnectionDirect(route.Proxy, true) + if err != nil { + return err + } - // - // initiating proxy connection - // - lib.Log("[%s] NETWORK initiate proxy connection to %q via %q", n.nodename, request.To, route.Proxy) - connection, err = n.getConnectionDirect(route.Proxy, true) - if err != nil { - return err } + cookie := n.proxy.Cookie + flags := n.proxy.Flags + if route, err_route := n.ResolveProxy(request.To); err_route == nil { + cookie = route.Cookie + if request.Flags.Enable == false { + flags = route.Flags + } + } privKey, _ := rsa.GenerateKey(rand.Reader, 2048) pubKey := x509.MarshalPKCS1PublicKey(&privKey.PublicKey) request.PublicKey = pubKey + request.Flags = flags - // create digest using nodename, cookie, peername and pubKey - request.Digest = generateProxyDigest(n.nodename, route.Cookie, request.To, pubKey) + // create digest using creation, cookie and pubKey. + // we can't use neither n.nodename or request.To, or request.ID - + // - anything that contains nodename or peername, because of etf.AtomMapping. + request.Digest = generateProxyDigest(n.creation, cookie, pubKey) - request.Flags = route.Flags - if request.Flags.Enable == false { - request.Flags = n.proxy.Flags - } - - request.Hop = route.MaxHop if request.Hop < 1 { request.Hop = DefaultProxyMaxHop } @@ -525,29 +618,35 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro if len(request.Path) < 2 { // reply error. there must be atleast 2 nodes - initiating and transit nodes lib.Log("[%s] NETWORK proxy. Proxy connect request has wrong path (too short)", n.nodename) - return ErrProxyConnect + return lib.ErrProxyConnect } peername := request.Path[len(request.Path)-1] + + if n.proxy.Accept == false { + lib.Warning("[%s] Got proxy connect request from %q. Not allowed.", n.nodename, peername) + return lib.ErrProxyConnect + } + cookie := n.proxy.Cookie flags := n.proxy.Flags - if has_route { + if route, err_route := n.ResolveProxy(peername); err_route == nil { cookie = route.Cookie - if route.Flags.Enable == true { + if request.Flags.Enable == false { flags = route.Flags } } - checkDigest := generateProxyDigest(peername, cookie, n.nodename, request.PublicKey) + checkDigest := generateProxyDigest(request.Creation, cookie, request.PublicKey) if bytes.Equal(request.Digest, checkDigest) == false { // reply error. digest mismatch lib.Log("[%s] NETWORK proxy. Proxy connect request has wrong digest", n.nodename) - return ErrProxyConnect + return lib.ErrProxyConnect } // do some encryption magic pk, err := x509.ParsePKCS1PublicKey(request.PublicKey) if err != nil { lib.Log("[%s] NETWORK proxy. Proxy connect request has wrong public key", n.nodename) - return ErrProxyConnect + return lib.ErrProxyConnect } hash := sha256.New() key := make([]byte, 32) @@ -555,7 +654,7 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro cipherkey, err := rsa.EncryptOAEP(hash, rand.Reader, pk, key, nil) if err != nil { lib.Log("[%s] NETWORK proxy. Proxy connect request. Can't encrypt: %s ", n.nodename, err) - return ErrProxyConnect + return lib.ErrProxyConnect } block, err := aes.NewCipher(key) if err != nil { @@ -563,7 +662,7 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro } sessionID := lib.RandomString(32) - digest := generateProxyDigest(n.nodename, n.proxy.Cookie, peername, key) + digest := generateProxyDigest(n.creation, n.proxy.Cookie, key) if flags.Enable == false { flags = DefaultProxyFlags() } @@ -579,7 +678,7 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro proxySessionID: sessionID, } if _, err := n.registerConnection(peername, cInternal); err != nil { - return ErrProxySessionDuplicate + return lib.ErrProxySessionDuplicate } reply := ProxyConnectReply{ @@ -597,7 +696,7 @@ func (n *network) RouteProxyConnectRequest(from ConnectionInterface, request Pro // can't send reply. ignore this connection request lib.Log("[%s] NETWORK proxy. Proxy connect request. Can't send reply: %s ", n.nodename, err) n.unregisterConnection(peername, nil) - return ErrProxyConnect + return lib.ErrProxyConnect } session := ProxySession{ @@ -621,25 +720,25 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo n.proxyTransitSessionsMutex.RUnlock() if duplicate { - return ErrProxySessionDuplicate + return lib.ErrProxySessionDuplicate } if from == nil { // from value can't be nil - return ErrProxyUnknownRequest + return lib.ErrProxyUnknownRequest } if reply.To != n.nodename { // send this reply further and register this session if n.proxy.Transit == false { - return ErrProxyTransitDisabled + return lib.ErrProxyTransitDisabled } if len(reply.Path) == 0 { - return ErrProxyUnknownRequest + return lib.ErrProxyUnknownRequest } if len(reply.Path) > defaultProxyPathLimit { - return ErrProxyPathTooLong + return lib.ErrProxyPathTooLong } next := reply.Path[0] @@ -648,14 +747,14 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo return err } if connection == from { - return ErrProxyLoopDetected + return lib.ErrProxyLoopDetected } reply.Path = reply.Path[1:] // check for the looping for i := range reply.Path { if reply.Path[i] == next { - return ErrProxyLoopDetected + return lib.ErrProxyLoopDetected } } @@ -690,7 +789,7 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo // look up for the request we made earlier r, found := n.getProxyConnectRequest(reply.ID) if found == false { - return ErrProxyUnknownRequest + return lib.ErrProxyUnknownRequest } // decrypt cipher key using private key @@ -698,7 +797,7 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo key, err := rsa.DecryptOAEP(hash, rand.Reader, r.privateKey, reply.Cipher, nil) if err != nil { lib.Log("[%s] CORE route proxy. Proxy connect reply has invalid cipher", n.nodename) - return ErrProxyConnect + return lib.ErrProxyConnect } cookie := n.proxy.Cookie @@ -710,10 +809,10 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo cookie = route.Cookie } // check digest - checkDigest := generateProxyDigest(r.request.To, cookie, n.nodename, key) + checkDigest := generateProxyDigest(reply.Creation, cookie, key) if bytes.Equal(checkDigest, reply.Digest) == false { lib.Log("[%s] CORE route proxy. Proxy connect reply has wrong digest", n.nodename) - return ErrProxyConnect + return lib.ErrProxyConnect } block, err := aes.NewCipher(key) @@ -728,7 +827,7 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo select { case r.connection <- registered: } - return ErrProxySessionDuplicate + return lib.ErrProxySessionDuplicate } // if one of the nodes want to use encryption then it must be used by both nodes if r.request.Flags.EnableEncryption || reply.Flags.EnableEncryption { @@ -758,7 +857,7 @@ func (n *network) RouteProxyConnectReply(from ConnectionInterface, reply ProxyCo func (n *network) RouteProxyConnectCancel(from ConnectionInterface, cancel ProxyConnectCancel) error { if from == nil { // from value can not be nil - return ErrProxyConnect + return lib.ErrProxyConnect } if len(cancel.Path) == 0 { n.cancelProxyConnectRequest(cancel) @@ -768,7 +867,7 @@ func (n *network) RouteProxyConnectCancel(from ConnectionInterface, cancel Proxy next := cancel.Path[0] if next != n.nodename { if len(cancel.Path) > defaultProxyPathLimit { - return ErrProxyPathTooLong + return lib.ErrProxyPathTooLong } connection, err := n.getConnectionDirect(next, false) if err != nil { @@ -776,14 +875,14 @@ func (n *network) RouteProxyConnectCancel(from ConnectionInterface, cancel Proxy } if connection == from { - return ErrProxyLoopDetected + return lib.ErrProxyLoopDetected } cancel.Path = cancel.Path[1:] // check for the looping for i := range cancel.Path { if cancel.Path[i] == next { - return ErrProxyLoopDetected + return lib.ErrProxyLoopDetected } } @@ -793,7 +892,7 @@ func (n *network) RouteProxyConnectCancel(from ConnectionInterface, cancel Proxy return nil } - return ErrProxyUnknownRequest + return lib.ErrProxyUnknownRequest } func (n *network) RouteProxyDisconnect(from ConnectionInterface, disconnect ProxyDisconnect) error { @@ -820,12 +919,12 @@ func (n *network) RouteProxyDisconnect(from ConnectionInterface, disconnect Prox } if found == false { n.connectionsMutex.RUnlock() - return ErrProxySessionUnknown + return lib.ErrProxySessionUnknown } n.connectionsMutex.RUnlock() if ci.proxySessionID != disconnect.SessionID || ci.connection != from { - return ErrProxySessionUnknown + return lib.ErrProxySessionUnknown } n.unregisterConnection(peername, &disconnect) @@ -881,7 +980,7 @@ func (n *network) RouteProxy(from ConnectionInterface, sessionID string, packet n.proxyTransitSessionsMutex.RUnlock() if !ok { - return ErrProxySessionUnknown + return lib.ErrProxySessionUnknown } switch from { @@ -895,11 +994,11 @@ func (n *network) RouteProxy(from ConnectionInterface, sessionID string, packet } } -func (n *network) AddProxyRoute(node string, route ProxyRoute) error { +func (n *network) AddProxyRoute(route ProxyRoute) error { n.proxyRoutesMutex.Lock() defer n.proxyRoutesMutex.Unlock() if route.MaxHop > defaultProxyPathLimit { - return ErrProxyPathTooLong + return lib.ErrProxyPathTooLong } if route.MaxHop < 1 { route.MaxHop = DefaultProxyMaxHop @@ -909,21 +1008,32 @@ func (n *network) AddProxyRoute(node string, route ProxyRoute) error { route.Flags = n.proxy.Flags } - if _, exist := n.proxyRoutes[node]; exist { - return ErrTaken + if s := strings.Split(route.Name, "@"); len(s) == 2 { + if s[0] == "" { + // must be domain name + if strings.HasPrefix(route.Name, "@") == false { + return lib.ErrRouteName + } + } + } else { + return lib.ErrRouteName } - n.proxyRoutes[node] = route + if _, exist := n.proxyRoutes[route.Name]; exist { + return lib.ErrTaken + } + + n.proxyRoutes[route.Name] = route return nil } -func (n *network) RemoveProxyRoute(node string) bool { +func (n *network) RemoveProxyRoute(name string) bool { n.proxyRoutesMutex.Lock() defer n.proxyRoutesMutex.Unlock() - if _, exist := n.proxyRoutes[node]; exist == false { + if _, exist := n.proxyRoutes[name]; exist == false { return false } - delete(n.proxyRoutes, node) + delete(n.proxyRoutes, name) return true } @@ -944,30 +1054,53 @@ func (n *network) ProxyRoute(name string) (ProxyRoute, bool) { return route, exist } -func (n *network) listen(ctx context.Context, hostname string, begin uint16, end uint16) (uint16, error) { +func (n *network) listen(ctx context.Context, hostname string, options Listener, register bool) (net.Listener, error) { lc := net.ListenConfig{ KeepAlive: defaultKeepAlivePeriod * time.Second, } - for port := begin; port <= end; port++ { + tlsEnabled := false + if options.TLS != nil { + if options.TLS.Certificates != nil || options.TLS.GetCertificate != nil { + tlsEnabled = true + } + } + + for port := options.ListenBegin; port <= options.ListenEnd; port++ { hostPort := net.JoinHostPort(hostname, strconv.Itoa(int(port))) listener, err := lc.Listen(ctx, "tcp", hostPort) if err != nil { continue } - if n.tls.Enable { - config := tls.Config{ - Certificates: []tls.Certificate{n.tls.Server}, - InsecureSkipVerify: n.tls.SkipVerify, + + if register && n.registrar != nil { + registerOptions := RegisterOptions{ + Port: port, + Creation: n.creation, + NodeVersion: n.version, + HandshakeVersion: options.Handshake.Version(), + EnableTLS: tlsEnabled, + EnableProxy: options.Flags.EnableProxy, + EnableCompression: options.Flags.EnableCompression, + } + + if err := n.registrar.Register(n.ctx, n.nodename, registerOptions); err != nil { + listener.Close() + return nil, err } - listener = tls.NewListener(listener, &config) } - n.listener = listener + + if tlsEnabled { + listener = tls.NewListener(listener, options.TLS) + } go func() { for { c, err := listener.Accept() if err != nil { + if err == io.EOF { + return + } if ctx.Err() == nil { continue } @@ -976,18 +1109,23 @@ func (n *network) listen(ctx context.Context, hostname string, begin uint16, end } lib.Log("[%s] NETWORK accepted new connection from %s", n.nodename, c.RemoteAddr().String()) - details, err := n.handshake.Accept(c, n.tls.Enable, n.cookie) + details, err := options.Handshake.Accept(c.RemoteAddr(), c, tlsEnabled, options.Cookie) if err != nil { - lib.Log("[%s] Can't handshake with %s: %s", n.nodename, c.RemoteAddr().String(), err) + if err != io.EOF { + lib.Warning("[%s] Can't handshake with %s: %s", n.nodename, c.RemoteAddr().String(), err) + } + c.Close() + continue + } + if details.Name == "" { + err := fmt.Errorf("remote node introduced itself as %q", details.Name) + lib.Warning("Handshake error: %s", err) c.Close() continue } - // TODO we need to detect somehow whether to enable software keepalive. - // Erlang nodes are required to be receiving keepalive messages, - // but Ergo doesn't need it. - details.Flags.EnableSoftwareKeepAlive = true - connection, err := n.proto.Init(n.ctx, c, n.nodename, details) + connection, err := options.Proto.Init(n.ctx, c, n.nodename, details) if err != nil { + lib.Warning("Proto error: %s", err) c.Close() continue } @@ -1008,86 +1146,66 @@ func (n *network) listen(ctx context.Context, hostname string, begin uint16, end // run serving connection go func(ctx context.Context, ci connectionInternal) { - n.proto.Serve(ci.connection, n.router) + options.Proto.Serve(ci.connection, n.router) n.unregisterConnection(details.Name, nil) - n.proto.Terminate(ci.connection) + options.Proto.Terminate(ci.connection) ci.conn.Close() }(ctx, cInternal) } }() - // return port number this node listenig on for the incoming connections - return port, nil + return listener, nil } // all ports within a given range are taken - return 0, fmt.Errorf("Can't start listener. Port range is taken") + return nil, fmt.Errorf("can not start listener (port range is taken)") } func (n *network) connect(node string) (ConnectionInterface, error) { - var route Route var c net.Conn - var err error - var enabledTLS bool lib.Log("[%s] NETWORK trying to connect to %#v", n.nodename, node) // resolve the route - route, err = n.Resolve(node) + route, err := n.Resolve(node) if err != nil { return nil, err } + customHandshake := route.Options.Handshake != nil + lib.Log("[%s] NETWORK resolved %#v to %s:%d (custom handshake: %t)", n.nodename, node, route.Host, route.Port, customHandshake) HostPort := net.JoinHostPort(route.Host, strconv.Itoa(int(route.Port))) dialer := net.Dialer{ KeepAlive: defaultKeepAlivePeriod * time.Second, } + tlsEnabled := route.Options.TLS != nil + if route.Options.IsErgo == true { - // rely on the route TLS settings if they were defined - if route.Options.EnableTLS { - if route.Options.Cert.Certificate == nil { - // use the local TLS settings - config := tls.Config{ - Certificates: []tls.Certificate{n.tls.Client}, - InsecureSkipVerify: n.tls.SkipVerify, - } - tlsdialer := tls.Dialer{ - NetDialer: &dialer, - Config: &config, - } - c, err = tlsdialer.DialContext(n.ctx, "tcp", HostPort) - } else { - // use the route TLS settings - config := tls.Config{ - Certificates: []tls.Certificate{route.Options.Cert}, - } - tlsdialer := tls.Dialer{ - NetDialer: &dialer, - Config: &config, - } - c, err = tlsdialer.DialContext(n.ctx, "tcp", HostPort) + // use the route TLS settings if they were defined + if tlsEnabled { + if n.tls != nil { + route.Options.TLS.InsecureSkipVerify = n.tls.InsecureSkipVerify } - enabledTLS = true - + // use the local TLS settings + tlsdialer := tls.Dialer{ + NetDialer: &dialer, + Config: route.Options.TLS, + } + c, err = tlsdialer.DialContext(n.ctx, "tcp", HostPort) } else { // TLS disabled on a remote node c, err = dialer.DialContext(n.ctx, "tcp", HostPort) } - } else { - // rely on the local TLS settings - if n.tls.Enable { - config := tls.Config{ - Certificates: []tls.Certificate{n.tls.Client}, - InsecureSkipVerify: n.tls.SkipVerify, - } + // this is an Erlang/Elixir node. use the local TLS settings + tlsEnabled = n.tls != nil + if tlsEnabled { tlsdialer := tls.Dialer{ NetDialer: &dialer, - Config: &config, + Config: n.tls, } c, err = tlsdialer.DialContext(n.ctx, "tcp", HostPort) - enabledTLS = true } else { c, err = dialer.DialContext(n.ctx, "tcp", HostPort) @@ -1096,6 +1214,7 @@ func (n *network) connect(node string) (ConnectionInterface, error) { // check if we couldn't establish a connection with the node if err != nil { + lib.Warning("Could not connect to %q (%s): %s", node, HostPort, err) return nil, err } @@ -1111,13 +1230,14 @@ func (n *network) connect(node string) (ConnectionInterface, error) { cookie = route.Options.Cookie } - details, err := n.handshake.Start(c, enabledTLS, cookie) + details, err := handshake.Start(c.RemoteAddr(), c, tlsEnabled, cookie) if err != nil { + lib.Warning("Handshake error: %s", err) c.Close() return nil, err } if details.Name != node { - err := fmt.Errorf("node %q introduced itself as %q", node, details.Name) + err := fmt.Errorf("Handshake error: node %q introduced itself as %q", node, details.Name) lib.Warning("%s", err) return nil, err } @@ -1129,13 +1249,10 @@ func (n *network) connect(node string) (ConnectionInterface, error) { proto = n.proto } - // TODO we need to detect somehow whether to enable software keepalive. - // Erlang nodes are required to be receiving keepalive messages, - // but Ergo doesn't need it. - details.Flags.EnableSoftwareKeepAlive = true - connection, err := n.proto.Init(n.ctx, c, n.nodename, details) + connection, err := proto.Init(n.ctx, c, n.nodename, details) if err != nil { c.Close() + lib.Warning("Proto error: %s", err) return nil, err } cInternal := connectionInternal{ @@ -1149,17 +1266,24 @@ func (n *network) connect(node string) (ConnectionInterface, error) { // connection to this node. // Close this connection and use the already registered one c.Close() - if err == ErrTaken { + if err == lib.ErrTaken { return registered, nil } return nil, err } + // enable keep alive on this connection + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(5 * time.Second) + tcp.SetNoDelay(true) + } + // run serving connection go func(ctx context.Context, ci connectionInternal) { - n.proto.Serve(ci.connection, n.router) + proto.Serve(ci.connection, n.router) n.unregisterConnection(details.Name, nil) - n.proto.Terminate(ci.connection) + proto.Terminate(ci.connection) ci.conn.Close() }(n.ctx, cInternal) @@ -1173,15 +1297,22 @@ func (n *network) registerConnection(peername string, ci connectionInternal) (Co if registered, exist := n.connections[peername]; exist { // already registered - return registered.connection, ErrTaken + return registered.connection, lib.ErrTaken } n.connections[peername] = ci + + event := MessageEventNetwork{ + PeerName: peername, + Online: true, + } if ci.conn == nil { // this is proxy connection p, _ := n.connectionsProxy[ci.connection] p = append(p, peername) n.connectionsProxy[ci.connection] = p + event.Proxy = true } + n.router.sendEvent(corePID, EventNetwork, event) return ci.connection, nil } @@ -1198,18 +1329,31 @@ func (n *network) unregisterConnection(peername string, disconnect *ProxyDisconn n.connectionsMutex.Unlock() n.router.RouteNodeDown(peername, disconnect) + event := MessageEventNetwork{ + PeerName: peername, + Online: false, + } if ci.conn == nil { // it was proxy connection ci.connection.ProxyUnregisterSession(ci.proxySessionID) + event.Proxy = true + n.router.sendEvent(corePID, EventNetwork, event) return } + n.router.sendEvent(corePID, EventNetwork, event) + + // we must unregister this peer for the proxy connection via this node + n.registrar.UnregisterProxy(peername) n.connectionsMutex.Lock() cp, _ := n.connectionsProxy[ci.connection] for _, p := range cp { lib.Log("[%s] NETWORK unregistering peer (via proxy) %v", n.nodename, p) delete(n.connections, p) + event.PeerName = p + event.Proxy = true + n.router.sendEvent(corePID, EventNetwork, event) } ct, _ := n.connectionsTransit[ci.connection] @@ -1243,83 +1387,88 @@ func (n *network) unregisterConnection(peername string, disconnect *ProxyDisconn // Connection interface default callbacks // func (c *Connection) Send(from gen.Process, to etf.Pid, message etf.Term) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) SendReg(from gen.Process, to gen.ProcessID, message etf.Term) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) SendAlias(from gen.Process, to etf.Alias, message etf.Term) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) Link(local gen.Process, remote etf.Pid) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) Unlink(local gen.Process, remote etf.Pid) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) LinkExit(local etf.Pid, remote etf.Pid, reason string) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) Monitor(local gen.Process, remote etf.Pid, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) MonitorReg(local gen.Process, remote gen.ProcessID, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) Demonitor(by etf.Pid, process etf.Pid, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) DemonitorReg(by etf.Pid, process gen.ProcessID, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) MonitorExitReg(process gen.Process, reason string, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) MonitorExit(to etf.Pid, terminated etf.Pid, reason string, ref etf.Ref) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) SpawnRequest(nodeName string, behaviorName string, request gen.RemoteSpawnRequest, args ...etf.Term) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) SpawnReply(to etf.Pid, ref etf.Ref, pid etf.Pid) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) SpawnReplyError(to etf.Pid, ref etf.Ref, err error) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyConnectRequest(connect ProxyConnectRequest) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyConnectReply(reply ProxyConnectReply) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyDisconnect(disconnect ProxyDisconnect) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyRegisterSession(session ProxySession) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyUnregisterSession(id string) error { - return ErrUnsupported + return lib.ErrUnsupported } func (c *Connection) ProxyPacket(packet *lib.Buffer) error { - return ErrUnsupported + return lib.ErrUnsupported +} +func (c *Connection) Stats() NetworkStats { + return NetworkStats{} } // // Handshake interface default callbacks // -func (h *Handshake) Start(c net.Conn) (Flags, error) { - return Flags{}, ErrUnsupported +func (h *Handshake) Start(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (HandshakeDetails, error) { + return HandshakeDetails{}, lib.ErrUnsupported } -func (h *Handshake) Accept(c net.Conn) (string, Flags, error) { - return "", Flags{}, ErrUnsupported +func (h *Handshake) Accept(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (HandshakeDetails, error) { + return HandshakeDetails{}, lib.ErrUnsupported } func (h *Handshake) Version() HandshakeVersion { var v HandshakeVersion return v } +// internals + func (n *network) putProxyConnectRequest(r proxyConnectRequest) { n.proxyConnectRequestMutex.Lock() defer n.proxyConnectRequestMutex.Unlock() @@ -1349,7 +1498,7 @@ func (n *network) waitProxyConnection(id etf.Ref, timeout int) (ConnectionInterf n.proxyConnectRequestMutex.RUnlock() if found == false { - return nil, ErrProxyUnknownRequest + return nil, lib.ErrProxyUnknownRequest } defer func(id etf.Ref) { @@ -1369,11 +1518,11 @@ func (n *network) waitProxyConnection(id etf.Ref, timeout int) (ConnectionInterf case err := <-r.cancel: return nil, fmt.Errorf("[%s] %s", err.From, err.Reason) case <-timer.C: - return nil, ErrTimeout + return nil, lib.ErrTimeout case <-n.ctx.Done(): // node is on the way to terminate, it means connection is closed // so it doesn't matter what kind of error will be returned - return nil, ErrProxyUnknownRequest + return nil, lib.ErrProxyUnknownRequest } } } @@ -1385,66 +1534,29 @@ func (n *network) getProxyConnectRequest(id etf.Ref) (proxyConnectRequest, bool) return r, found } +func (n *network) networkStats() internalNetworkStats { + stats := internalNetworkStats{} + n.proxyTransitSessionsMutex.RLock() + stats.transitConnections = len(n.proxyTransitSessions) + n.proxyTransitSessionsMutex.RUnlock() + + n.connectionsMutex.RLock() + stats.proxyConnections = len(n.connectionsProxy) + stats.connections = len(n.connections) + n.connectionsMutex.RUnlock() + return stats +} + // // internals // -func generateProxyDigest(node string, cookie string, peer string, pubkey []byte) []byte { +func generateProxyDigest(creation uint32, cookie string, pubkey []byte) []byte { // md5(md5(md5(md5(node)+cookie)+peer)+pubkey) - digest1 := md5.Sum([]byte(node)) + c := [4]byte{} + binary.BigEndian.PutUint32(c[:], creation) + digest1 := md5.Sum([]byte(c[:])) digest2 := md5.Sum(append(digest1[:], []byte(cookie)...)) - digest3 := md5.Sum(append(digest2[:], []byte(peer)...)) - digest4 := md5.Sum(append(digest3[:], pubkey...)) - return digest4[:] -} - -func generateSelfSignedCert(version Version) (tls.Certificate, error) { - var cert = tls.Certificate{} - org := fmt.Sprintf("%s %s", version.Prefix, version.Release) - certPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - if err != nil { - return cert, err - } - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return cert, err - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{org}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 365), - //IsCA: true, - - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1")) - - certBytes, err1 := x509.CreateCertificate(rand.Reader, &template, &template, - &certPrivKey.PublicKey, certPrivKey) - if err1 != nil { - return cert, err1 - } - - certPEM := new(bytes.Buffer) - pem.Encode(certPEM, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - }) - - certPrivKeyPEM := new(bytes.Buffer) - x509Encoded, _ := x509.MarshalECPrivateKey(certPrivKey) - pem.Encode(certPrivKeyPEM, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509Encoded, - }) - - return tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes()) + digest3 := md5.Sum(append(digest2[:], pubkey...)) + return digest3[:] } diff --git a/node/node.go b/node/node.go index 3d3c3730..b93092d2 100644 --- a/node/node.go +++ b/node/node.go @@ -44,29 +44,14 @@ func StartWithContext(ctx context.Context, name string, cookie string, opts Opti opts.Flags = DefaultFlags() } - // set defaults listening port range - if opts.Listen > 0 { - opts.ListenBegin = opts.Listen - opts.ListenEnd = opts.Listen - lib.Log("Node listening port: %d", opts.Listen) - } else { - if opts.ListenBegin == 0 { - opts.ListenBegin = defaultListenBegin - } - if opts.ListenEnd == 0 { - opts.ListenEnd = defaultListenEnd - } - lib.Log("Node listening range: %d...%d", opts.ListenBegin, opts.ListenEnd) - } - if opts.Handshake == nil { return nil, fmt.Errorf("Handshake must be defined") } if opts.Proto == nil { return nil, fmt.Errorf("Proto must be defined") } - if opts.StaticRoutesOnly == false && opts.Resolver == nil { - return nil, fmt.Errorf("Resolver must be defined if StaticRoutesOnly == false") + if opts.StaticRoutesOnly == false && opts.Registrar == nil { + return nil, fmt.Errorf("Registrar must be defined if StaticRoutesOnly == false") } nodectx, nodestop := context.WithCancel(ctx) @@ -160,6 +145,33 @@ func (n *node) Wait() { n.coreWait() } +func (n *node) Stats() NodeStats { + stats := NodeStats{} + + coreStats := n.coreStats() + stats.TotalProcesses = coreStats.totalProcesses + stats.TotalReferences = coreStats.totalReferences + stats.RunningProcesses = uint64(coreStats.processes) + stats.RegisteredNames = uint64(coreStats.names) + stats.RegisteredAliases = uint64(coreStats.aliases) + + monStats := n.monitorStats() + stats.MonitorsByPid = uint64(monStats.monitorsByPid) + stats.MonitorsByName = uint64(monStats.monitorsByName) + stats.MonitorsNodes = uint64(monStats.monitorsNodes) + stats.Links = uint64(monStats.links) + + stats.LoadedApplications = uint64(len(n.LoadedApplications())) + stats.RunningApplications = uint64(len(n.WhichApplications())) + + netStats := n.networkStats() + stats.NetworkConnections = uint64(netStats.connections) + stats.ProxyConnections = uint64(netStats.proxyConnections) + stats.TransitConnections = uint64(netStats.transitConnections) + + return stats +} + // WaitWithTimeout func (n *node) WaitWithTimeout(d time.Duration) error { return n.coreWaitWithTimeout(d) @@ -206,11 +218,11 @@ func (n *node) listApplications(onlyRunning bool) []gen.ApplicationInfo { func (n *node) ApplicationInfo(name string) (gen.ApplicationInfo, error) { rb, err := n.RegisteredBehavior(appBehaviorGroup, name) if err != nil { - return gen.ApplicationInfo{}, ErrAppUnknown + return gen.ApplicationInfo{}, lib.ErrAppUnknown } spec, ok := rb.Data.(*gen.ApplicationSpec) if !ok { - return gen.ApplicationInfo{}, ErrAppUnknown + return gen.ApplicationInfo{}, lib.ErrAppUnknown } pid := etf.Pid{} @@ -246,15 +258,15 @@ func (n *node) ApplicationLoad(app gen.ApplicationBehavior, args ...etf.Term) (s func (n *node) ApplicationUnload(appName string) error { rb, err := n.RegisteredBehavior(appBehaviorGroup, appName) if err != nil { - return ErrAppUnknown + return lib.ErrAppUnknown } spec, ok := rb.Data.(*gen.ApplicationSpec) if !ok { - return ErrAppUnknown + return lib.ErrAppUnknown } if spec.Process != nil { - return ErrAppAlreadyStarted + return lib.ErrAppAlreadyStarted } return n.UnregisterBehavior(appBehaviorGroup, appName) @@ -285,12 +297,12 @@ func (n *node) ApplicationStart(appName string, args ...etf.Term) (gen.Process, func (n *node) applicationStart(startType, appName string, args ...etf.Term) (gen.Process, error) { rb, err := n.RegisteredBehavior(appBehaviorGroup, appName) if err != nil { - return nil, ErrAppUnknown + return nil, lib.ErrAppUnknown } spec, ok := rb.Data.(*gen.ApplicationSpec) if !ok { - return nil, ErrAppUnknown + return nil, lib.ErrAppUnknown } spec.StartType = startType @@ -301,12 +313,12 @@ func (n *node) applicationStart(startType, appName string, args ...etf.Term) (ge defer spec.Unlock() if spec.Process != nil { - return nil, ErrAppAlreadyStarted + return nil, lib.ErrAppAlreadyStarted } // start dependencies for _, depAppName := range spec.Applications { - if _, e := n.ApplicationStart(depAppName); e != nil && e != ErrAppAlreadyStarted { + if _, e := n.ApplicationStart(depAppName); e != nil && e != lib.ErrAppAlreadyStarted { return nil, e } } @@ -329,18 +341,18 @@ func (n *node) applicationStart(startType, appName string, args ...etf.Term) (ge func (n *node) ApplicationStop(name string) error { rb, err := n.RegisteredBehavior(appBehaviorGroup, name) if err != nil { - return ErrAppUnknown + return lib.ErrAppUnknown } spec, ok := rb.Data.(*gen.ApplicationSpec) if !ok { - return ErrAppUnknown + return lib.ErrAppUnknown } spec.Lock() defer spec.Unlock() if spec.Process == nil { - return ErrAppIsNotRunning + return lib.ErrAppIsNotRunning } if e := spec.Process.Exit("normal"); e != nil { @@ -348,7 +360,7 @@ func (n *node) ApplicationStop(name string) error { } // we should wait until children process stopped. if e := spec.Process.WaitWithTimeout(5 * time.Second); e != nil { - return ErrProcessBusy + return lib.ErrProcessBusy } return nil } @@ -442,6 +454,15 @@ func DefaultFlags() Flags { } } +// DefaultCloudFlags +func DefaultCloudFlags() CloudFlags { + return CloudFlags{ + Enable: true, + EnableIntrospection: true, + EnableMetrics: true, + } +} + func DefaultProxyFlags() ProxyFlags { return ProxyFlags{ Enable: true, @@ -459,6 +480,10 @@ func DefaultProtoOptions() ProtoOptions { MaxMessageSize: 0, // no limit SendQueueLength: DefaultProtoSendQueueLength, RecvQueueLength: DefaultProtoRecvQueueLength, - FragmentationUnit: DefaultProroFragmentationUnit, + FragmentationUnit: DefaultProtoFragmentationUnit, } } + +func DefaultListener() Listener { + return Listener{} +} diff --git a/node/process.go b/node/process.go index 4e238bb9..7e4454c5 100644 --- a/node/process.go +++ b/node/process.go @@ -11,25 +11,19 @@ import ( "github.com/ergo-services/ergo/lib" ) -const ( - // DefaultProcessMailboxSize - DefaultProcessMailboxSize = 100 -) - var ( syncReplyChannels = &sync.Pool{ New: func() interface{} { - return make(chan etf.Term, 2) - }, - } - - directChannels = &sync.Pool{ - New: func() interface{} { - return make(chan gen.ProcessDirectMessage, 1) + return make(chan syncReplyMessage, 2) }, } ) +type syncReplyMessage struct { + value etf.Term + err error +} + type process struct { coreInternal sync.RWMutex @@ -52,7 +46,7 @@ type process struct { exit processExitFunc replyMutex sync.RWMutex - reply map[etf.Ref]chan etf.Term + reply map[etf.Ref]chan syncReplyMessage trapExit bool compression Compression @@ -80,7 +74,7 @@ func (p *process) Name() string { // RegisterName func (p *process) RegisterName(name string) error { if p.behavior == nil { - return ErrProcessTerminated + return lib.ErrProcessTerminated } return p.registerName(name, p.self) } @@ -88,14 +82,14 @@ func (p *process) RegisterName(name string) error { // UnregisterName func (p *process) UnregisterName(name string) error { if p.behavior == nil { - return ErrProcessTerminated + return lib.ErrProcessTerminated } prc := p.ProcessByName(name) if prc == nil { - return ErrNameUnknown + return lib.ErrNameUnknown } if prc.Self() != p.self { - return ErrNameOwner + return lib.ErrNameOwner } return p.unregisterName(name) } @@ -111,7 +105,7 @@ func (p *process) Kill() { // Exit func (p *process) Exit(reason string) error { if p.behavior == nil { - return ErrProcessTerminated + return lib.ErrProcessTerminated } return p.exit(p.self, reason) } @@ -164,7 +158,9 @@ func (p *process) Aliases() []etf.Alias { // Info func (p *process) Info() gen.ProcessInfo { + p.RLock() if p.behavior == nil { + p.RUnlock() return gen.ProcessInfo{} } @@ -172,6 +168,8 @@ func (p *process) Info() gen.ProcessInfo { if p.groupLeader != nil { gl = p.groupLeader.Self() } + p.RUnlock() + links := p.Links() monitors := p.Monitors() monitorsByName := p.MonitorsByName() @@ -194,9 +192,13 @@ func (p *process) Info() gen.ProcessInfo { // Send func (p *process) Send(to interface{}, message etf.Term) error { + p.RLock() if p.behavior == nil { - return ErrProcessTerminated + p.RUnlock() + return lib.ErrProcessTerminated } + p.RUnlock() + switch receiver := to.(type) { case etf.Pid: return p.RouteSend(p.self, receiver, message) @@ -213,40 +215,31 @@ func (p *process) Send(to interface{}, message etf.Term) error { } // SendAfter -func (p *process) SendAfter(to interface{}, message etf.Term, after time.Duration) context.CancelFunc { - //TODO: should we control the number of timers/goroutines have been created this way? - ctx, cancel := context.WithCancel(p.context) - go func() { - // to prevent of timer leaks due to its not GCed until the timer fires - timer := time.NewTimer(after) - defer timer.Stop() - defer cancel() +func (p *process) SendAfter(to interface{}, message etf.Term, after time.Duration) gen.CancelFunc { - select { - case <-ctx.Done(): - return - case <-timer.C: - if p.IsAlive() { - p.Send(to, message) - } - } - }() - return cancel + timer := time.AfterFunc(after, func() { p.Send(to, message) }) + return timer.Stop } // CreateAlias func (p *process) CreateAlias() (etf.Alias, error) { + p.RLock() if p.behavior == nil { - return etf.Alias{}, ErrProcessTerminated + p.RUnlock() + return etf.Alias{}, lib.ErrProcessTerminated } + p.RUnlock() return p.newAlias(p) } // DeleteAlias func (p *process) DeleteAlias(alias etf.Alias) error { + p.RLock() if p.behavior == nil { - return ErrProcessTerminated + p.RUnlock() + return lib.ErrProcessTerminated } + p.RUnlock() return p.deleteAlias(p, alias) } @@ -278,6 +271,7 @@ func (p *process) ListEnv() map[gen.EnvKey]interface{} { func (p *process) SetEnv(name gen.EnvKey, value interface{}) { p.Lock() defer p.Unlock() + if value == nil { delete(p.env, name) return @@ -319,7 +313,7 @@ func (p *process) WaitWithTimeout(d time.Duration) error { select { case <-timer.C: - return ErrTimeout + return lib.ErrTimeout case <-p.context.Done(): return nil } @@ -327,22 +321,30 @@ func (p *process) WaitWithTimeout(d time.Duration) error { // Link func (p *process) Link(with etf.Pid) error { + p.RLock() if p.behavior == nil { - return ErrProcessTerminated + p.RUnlock() + return lib.ErrProcessTerminated } + p.RUnlock() return p.RouteLink(p.self, with) } // Unlink func (p *process) Unlink(with etf.Pid) error { + p.RLock() if p.behavior == nil { - return ErrProcessTerminated + p.RUnlock() + return lib.ErrProcessTerminated } + p.RUnlock() return p.RouteUnlink(p.self, with) } // IsAlive func (p *process) IsAlive() bool { + p.RLock() + defer p.RUnlock() if p.behavior == nil { return false } @@ -366,7 +368,7 @@ func (p *process) NodeUptime() int64 { // Children func (p *process) Children() ([]etf.Pid, error) { - c, err := p.directRequest(gen.MessageDirectChildren{}, 5) + c, err := p.Direct(gen.MessageDirectChildren{}) if err != nil { return []etf.Pid{}, err } @@ -427,8 +429,9 @@ func (p *process) SetCompressionThreshold(threshold int) bool { // Behavior func (p *process) Behavior() gen.ProcessBehavior { - p.Lock() - defer p.Unlock() + p.RLock() + defer p.RUnlock() + if p.behavior == nil { return nil } @@ -437,15 +440,53 @@ func (p *process) Behavior() gen.ProcessBehavior { // Direct func (p *process) Direct(request interface{}) (interface{}, error) { - return p.directRequest(request, gen.DefaultCallTimeout) + return p.DirectWithTimeout(request, gen.DefaultCallTimeout) } // DirectWithTimeout func (p *process) DirectWithTimeout(request interface{}, timeout int) (interface{}, error) { if timeout < 1 { - timeout = 5 + timeout = gen.DefaultCallTimeout } - return p.directRequest(request, timeout) + + direct := gen.ProcessDirectMessage{ + Ref: p.MakeRef(), + Message: request, + } + + if err := p.PutSyncRequest(direct.Ref); err != nil { + return nil, err + } + + // sending request + select { + case p.direct <- direct: + default: + p.CancelSyncRequest(direct.Ref) + return nil, lib.ErrProcessBusy + } + + return p.WaitSyncReply(direct.Ref, timeout) +} + +func (p *process) RegisterEvent(event gen.Event, messages ...gen.EventMessage) error { + return p.registerEvent(p.self, event, messages) +} + +func (p *process) UnregisterEvent(event gen.Event) error { + return p.unregisterEvent(p.self, event) +} + +func (p *process) MonitorEvent(event gen.Event) error { + return p.monitorEvent(p.self, event) +} + +func (p *process) DemonitorEvent(event gen.Event) error { + return p.demonitorEvent(p.self, event) +} + +func (p *process) SendEventMessage(event gen.Event, message gen.EventMessage) error { + return p.sendEvent(p.self, event, message) } // MonitorNode @@ -531,10 +572,10 @@ func (p *process) RemoteSpawnWithTimeout(timeout int, node string, object string return r, nil case etf.Atom: switch string(r) { - case ErrTaken.Error(): - return etf.Pid{}, ErrTaken - case ErrBehaviorUnknown.Error(): - return etf.Pid{}, ErrBehaviorUnknown + case lib.ErrTaken.Error(): + return etf.Pid{}, lib.ErrTaken + case lib.ErrBehaviorUnknown.Error(): + return etf.Pid{}, lib.ErrBehaviorUnknown } return etf.Pid{}, fmt.Errorf(string(r)) } @@ -551,74 +592,54 @@ func (p *process) Spawn(name string, opts gen.ProcessOptions, behavior gen.Proce return p.spawn(name, options, behavior, args...) } -func (p *process) directRequest(request interface{}, timeout int) (interface{}, error) { - if p.direct == nil { - return nil, ErrProcessTerminated - } - - timer := lib.TakeTimer() - defer lib.ReleaseTimer(timer) - - direct := gen.ProcessDirectMessage{ - Message: request, - Reply: directChannels.Get().(chan gen.ProcessDirectMessage), - } - - // sending request - select { - case p.direct <- direct: - timer.Reset(time.Second * time.Duration(timeout)) - case <-timer.C: - return nil, ErrProcessBusy - } - - // receiving response - select { - case response := <-direct.Reply: - directChannels.Put(direct.Reply) - if response.Err != nil { - return nil, response.Err - } - - return response.Message, nil - case <-timer.C: - return nil, ErrTimeout - } -} - // PutSyncRequest -func (p *process) PutSyncRequest(ref etf.Ref) { +func (p *process) PutSyncRequest(ref etf.Ref) error { + p.RLock() if p.reply == nil { - return + p.RUnlock() + return lib.ErrProcessTerminated } - reply := syncReplyChannels.Get().(chan etf.Term) + p.RUnlock() + + reply := syncReplyChannels.Get().(chan syncReplyMessage) p.replyMutex.Lock() p.reply[ref] = reply p.replyMutex.Unlock() + return nil } // PutSyncReply -func (p *process) PutSyncReply(ref etf.Ref, reply etf.Term) error { +func (p *process) PutSyncReply(ref etf.Ref, reply etf.Term, err error) error { + p.RLock() if p.reply == nil { - return ErrProcessTerminated + p.RUnlock() + return lib.ErrProcessTerminated } + p.RUnlock() p.replyMutex.RLock() rep, ok := p.reply[ref] - p.replyMutex.RUnlock() + defer p.replyMutex.RUnlock() if !ok { - // ignore this reply, no process waiting for it - return nil + // no process waiting for it + return lib.ErrReferenceUnknown } select { - case rep <- reply: + case rep <- syncReplyMessage{value: reply, err: err}: } return nil } // CancelSyncRequest func (p *process) CancelSyncRequest(ref etf.Ref) { + p.RLock() + if p.reply == nil { + p.RUnlock() + return + } + p.RUnlock() + p.replyMutex.Lock() delete(p.reply, ref) p.replyMutex.Unlock() @@ -626,15 +647,18 @@ func (p *process) CancelSyncRequest(ref etf.Ref) { // WaitSyncReply func (p *process) WaitSyncReply(ref etf.Ref, timeout int) (etf.Term, error) { + p.RLock() if p.reply == nil { - return nil, ErrProcessTerminated + p.RUnlock() + return nil, lib.ErrProcessTerminated } + p.RUnlock() p.replyMutex.RLock() reply, wait_for_reply := p.reply[ref] p.replyMutex.RUnlock() - if !wait_for_reply { + if wait_for_reply == false { return nil, fmt.Errorf("Unknown request") } @@ -651,12 +675,13 @@ func (p *process) WaitSyncReply(ref etf.Ref, timeout int) (etf.Term, error) { for { select { case m := <-reply: + // get back 'reply' struct to the pool syncReplyChannels.Put(reply) - return m, nil + return m.value, m.err case <-timer.C: - return nil, ErrTimeout + return nil, lib.ErrTimeout case <-p.context.Done(): - return nil, ErrProcessTerminated + return nil, lib.ErrProcessTerminated } } diff --git a/node/types.go b/node/types.go index a4e97915..cedfd82d 100644 --- a/node/types.go +++ b/node/types.go @@ -4,8 +4,7 @@ import ( "context" "crypto/cipher" "crypto/tls" - "fmt" - "io" + "net" "time" "github.com/ergo-services/ergo/etf" @@ -13,42 +12,6 @@ import ( "github.com/ergo-services/ergo/lib" ) -var ( - ErrAppAlreadyLoaded = fmt.Errorf("application is already loaded") - ErrAppAlreadyStarted = fmt.Errorf("application is already started") - ErrAppUnknown = fmt.Errorf("unknown application name") - ErrAppIsNotRunning = fmt.Errorf("application is not running") - ErrNameUnknown = fmt.Errorf("unknown name") - ErrNameOwner = fmt.Errorf("not an owner") - ErrProcessBusy = fmt.Errorf("process is busy") - ErrProcessUnknown = fmt.Errorf("unknown process") - ErrProcessIncarnation = fmt.Errorf("process ID belongs to the previous incarnation") - ErrProcessTerminated = fmt.Errorf("process terminated") - ErrMonitorUnknown = fmt.Errorf("unknown monitor reference") - ErrSenderUnknown = fmt.Errorf("unknown sender") - ErrBehaviorUnknown = fmt.Errorf("unknown behavior") - ErrBehaviorGroupUnknown = fmt.Errorf("unknown behavior group") - ErrAliasUnknown = fmt.Errorf("unknown alias") - ErrAliasOwner = fmt.Errorf("not an owner") - ErrNoRoute = fmt.Errorf("no route to node") - ErrTaken = fmt.Errorf("resource is taken") - ErrTimeout = fmt.Errorf("timed out") - ErrFragmented = fmt.Errorf("fragmented data") - - ErrUnsupported = fmt.Errorf("not supported") - ErrPeerUnsupported = fmt.Errorf("peer does not support this feature") - - ErrProxyUnknownRequest = fmt.Errorf("unknown proxy request") - ErrProxyTransitDisabled = fmt.Errorf("proxy feature disabled") - ErrProxyNoRoute = fmt.Errorf("no proxy route to node") - ErrProxyConnect = fmt.Errorf("can't establish proxy connection") - ErrProxyHopExceeded = fmt.Errorf("proxy hop is exceeded") - ErrProxyLoopDetected = fmt.Errorf("proxy loop detected") - ErrProxyPathTooLong = fmt.Errorf("proxy path too long") - ErrProxySessionUnknown = fmt.Errorf("unknown session id") - ErrProxySessionDuplicate = fmt.Errorf("session is already exist") -) - const ( // node options defaultListenBegin uint16 = 15000 @@ -56,18 +19,23 @@ const ( defaultKeepAlivePeriod time.Duration = 15 defaultProxyPathLimit int = 32 + DefaultProcessMailboxSize int = 100 + DefaultProcessDirectboxSize int = 10 + EnvKeyVersion gen.EnvKey = "ergo:Version" EnvKeyNode gen.EnvKey = "ergo:Node" EnvKeyRemoteSpawn gen.EnvKey = "ergo:RemoteSpawn" DefaultProtoRecvQueueLength int = 100 DefaultProtoSendQueueLength int = 100 - DefaultProroFragmentationUnit int = 65000 + DefaultProtoFragmentationUnit int = 65000 DefaultCompressionLevel int = -1 DefaultCompressionThreshold int = 1024 DefaultProxyMaxHop int = 8 + + EventNetwork gen.Event = "network" ) type Node interface { @@ -123,28 +91,38 @@ type Node interface { // StaticRoute returns Route for the given name. Returns false if it doesn't exist. StaticRoute(name string) (Route, bool) - AddProxyRoute(name string, proxy ProxyRoute) error + AddProxyRoute(proxy ProxyRoute) error RemoveProxyRoute(name string) bool + // ProxyRoutes returns list of proxy routes added using AddProxyRoute ProxyRoutes() []ProxyRoute + // ProxyRoute returns proxy route added using AddProxyRoute ProxyRoute(name string) (ProxyRoute, bool) // Resolve - Resolve(peername string) (Route, error) + Resolve(node string) (Route, error) + // ResolveProxy resolves proxy route. Checks for the proxy route added using AddProxyRoute. + // If it wasn't found makes request to the registrar. + ResolveProxy(node string) (ProxyRoute, error) // Connect sets up a connection to node - Connect(nodename string) error + Connect(node string) error // Disconnect close connection to the node - Disconnect(nodename string) error + Disconnect(node string) error // Nodes returns the list of connected nodes Nodes() []string // NodesIndirect returns the list of nodes connected via proxies NodesIndirect() []string + // NetworkStats returns network statistics of the connection with the node. Returns error + // ErrUnknown if connection with given node is not established. + NetworkStats(name string) (NetworkStats, error) Links(process etf.Pid) []etf.Pid Monitors(process etf.Pid) []etf.Pid MonitorsByName(process etf.Pid) []gen.ProcessID MonitoredBy(process etf.Pid) []etf.Pid + Stats() NodeStats + Stop() Wait() WaitWithTimeout(d time.Duration) error @@ -223,28 +201,25 @@ type Options struct { // Creation. Default value: uint32(time.Now().Unix()) Creation uint32 - // Flags defines enabled options for the running node - Flags Flags + // Listeners node can have multiple listening interface at once. If this list is empty + // the default listener will be using. Only the first listener will be registered on + // the Registrar + Listeners []Listener - // Listen defines a listening port number for accepting incoming connections. - Listen uint16 - // ListenBegin and ListenEnd define a range of the port numbers where - // the node looking for available free port number for the listening. - // Default values 15000 and 65000 accordingly - ListenBegin uint16 - ListenEnd uint16 + // Flags defines option flags of this node for the outgoing connection + Flags Flags // TLS settings - TLS TLS + TLS *tls.Config // StaticRoutesOnly disables resolving service (default is EPMD client) and // makes resolving localy only for nodes added using gen.AddStaticRoute StaticRoutesOnly bool - // Resolver defines a resolving service (default is EPMD service, client and server) - Resolver Resolver + // Registrar defines a registrar service (default is EPMD service, client and server) + Registrar Registrar - // Compression enables compression for outgoing messages (if peer node has this feature enabled) + // Compression defines default compression options for the spawning processes. Compression Compression // Handshake defines a handshake handler. By default is using @@ -255,33 +230,61 @@ type Options struct { // DIST proto created with dist.CreateProto(...) Proto ProtoInterface - // Proxy enable proxy feature on this node. Disabling this option makes - // this node to reject any proxy request. + // Cloud enable Ergo Cloud support + Cloud Cloud + + // Proxy options Proxy Proxy + + // System options for the system application + System System } -type TLS struct { - Enable bool - Server tls.Certificate - Client tls.Certificate - SkipVerify bool +type Listener struct { + // Cookie cookie for the incoming connection to this listener. Leave it empty in + // case of using the node's cookie. + Cookie string + // Listen defines a listening port number for accepting incoming connections. + Listen uint16 + // ListenBegin and ListenEnd define a range of the port numbers where + // the node looking for available free port number for the listening. + // Default values 15000 and 65000 accordingly + ListenBegin uint16 + ListenEnd uint16 + // Handshake if its nil the default TLS (Options.TLS) will be using + TLS *tls.Config + // Handshake if its nil the default Handshake (Options.Handshake) will be using + Handshake HandshakeInterface + // Proto if its nil the default Proto (Options.Proto) will be using + Proto ProtoInterface + // Flags defines option flags of this node for the incoming connection + // on this port. If its disabled the default Flags (Options.Flags) will be using + Flags Flags } type Cloud struct { - Enable bool - ID string - Cookie string + Enable bool + Cluster string + Cookie string + Flags CloudFlags + Timeout time.Duration } type Proxy struct { + // Transit allows to use this node as a proxy Transit bool - Flags ProxyFlags - Cookie string // set cookie for incoming connection - Routes map[string]ProxyRoute + // Accept incoming proxy connections + Accept bool + // Cookie sets cookie for incoming connections + Cookie string + // Flags sets options for incoming connections + Flags ProxyFlags + // Routes sets options for outgoing connections + Routes map[string]ProxyRoute } -type Metrics struct { - Disable bool +type System struct { + DisableAnonMetrics bool } type Compression struct { @@ -331,6 +334,7 @@ type ConnectionInterface interface { ProxyPacket(packet *lib.Buffer) error Creation() uint32 + Stats() NetworkStats } // Handshake template struct for the custom Handshake implementation @@ -340,14 +344,19 @@ type Handshake struct { // Handshake defines handshake interface type HandshakeInterface interface { + // Mandatory: + // Init initialize handshake. Init(nodename string, creation uint32, flags Flags) error + + // Optional: + // Start initiates handshake process. Argument tls means the connection is wrapped by TLS // Returns the name of connected peer, Flags and Creation wrapped into HandshakeDetails struct - Start(conn io.ReadWriter, tls bool, cookie string) (HandshakeDetails, error) + Start(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (HandshakeDetails, error) // Accept accepts handshake process initiated by another side of this connection. // Returns the name of connected peer, Flags and Creation wrapped into HandshakeDetails struct - Accept(conn io.ReadWriter, tls bool, cookie string) (HandshakeDetails, error) + Accept(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (HandshakeDetails, error) // Version handshake version. Must be implemented if this handshake is going to be used // for the accepting connections (this method is used in registration on the Resolver) Version() HandshakeVersion @@ -355,11 +364,20 @@ type HandshakeInterface interface { // HandshakeDetails type HandshakeDetails struct { - Name string - Flags Flags + // Name node name + Name string + // Flags node flags + Flags Flags + // Creation Creation uint32 - Version int - Custom HandshakeCustomDetails + // Version + Version int + // NumHandlers defines the number of readers/writers per connection. Default value is provided by ProtoOptions + NumHandlers int + // AtomMapping + AtomMapping etf.AtomMapping + // Custom allows passing the custom data to the ProtoInterface.Start + Custom HandshakeCustomDetails } type HandshakeCustomDetails interface{} @@ -374,7 +392,7 @@ type Proto struct { // Proto defines proto interface for the custom Proto implementation type ProtoInterface interface { // Init initialize connection handler - Init(ctx context.Context, conn io.ReadWriter, nodename string, details HandshakeDetails) (ConnectionInterface, error) + Init(ctx context.Context, conn lib.NetReadWriter, nodename string, details HandshakeDetails) (ConnectionInterface, error) // Serve connection Serve(connection ConnectionInterface, router CoreRouter) // Terminate invoked once Serve callback is finished @@ -420,24 +438,33 @@ type Flags struct { EnableCompression bool // Proxy enables support for incoming proxy connection EnableProxy bool - // Software keepalive enables sending keep alive messages if node doesn't support - // TCPConn.SetKeepAlive(true). For erlang peers this flag is mandatory. - EnableSoftwareKeepAlive bool } -// Resolver defines resolving interface -type Resolver interface { - Register(ctx context.Context, nodename string, port uint16, options ResolverOptions) error +// Registrar defines registrar interface +type Registrar interface { + Register(ctx context.Context, nodename string, options RegisterOptions) error + RegisterProxy(nodename string, maxhop int, flags ProxyFlags) error + UnregisterProxy(peername string) error Resolve(peername string) (Route, error) + ResolveProxy(peername string) (ProxyRoute, error) + Config() (RegistrarConfig, error) } -// ResolverOptions defines resolving options -type ResolverOptions struct { +type RegistrarConfig struct { + Version int + Config map[string]etf.Term +} + +// RegisterOptions defines resolving options +type RegisterOptions struct { + Port uint16 + Creation uint32 NodeVersion Version HandshakeVersion HandshakeVersion EnableTLS bool EnableProxy bool EnableCompression bool + Proxy string } // Route @@ -451,22 +478,30 @@ type Route struct { // RouteOptions type RouteOptions struct { Cookie string - EnableTLS bool + TLS *tls.Config IsErgo bool - Cert tls.Certificate Handshake HandshakeInterface Proto ProtoInterface - Custom CustomRouteOptions } // ProxyRoute type ProxyRoute struct { + // Name can be either nodename (example@domain) or domain (@domain) + Name string Proxy string Cookie string Flags ProxyFlags MaxHop int // DefaultProxyMaxHop == 8 } +// CloudFlags +type CloudFlags struct { + Enable bool + EnableIntrospection bool + EnableMetrics bool + EnableRemoteSpawn bool +} + // ProxyFlags type ProxyFlags struct { Enable bool @@ -526,5 +561,38 @@ type ProxySession struct { Block cipher.Block // made from symmetric key } -// CustomRouteOptions a custom set of route options -type CustomRouteOptions interface{} +type NetworkStats struct { + NodeName string + BytesIn uint64 + BytesOut uint64 + TransitBytesIn uint64 + TransitBytesOut uint64 + MessagesIn uint64 + MessagesOut uint64 +} + +type NodeStats struct { + TotalProcesses uint64 + TotalReferences uint64 + RunningProcesses uint64 + RegisteredNames uint64 + RegisteredAliases uint64 + + MonitorsByPid uint64 + MonitorsByName uint64 + MonitorsNodes uint64 + Links uint64 + + LoadedApplications uint64 + RunningApplications uint64 + + NetworkConnections uint64 + ProxyConnections uint64 + TransitConnections uint64 +} + +type MessageEventNetwork struct { + PeerName string + Online bool + Proxy bool +} diff --git a/proto/dist/epmd.go b/proto/dist/epmd.go index d1b80d7e..4e46f99a 100644 --- a/proto/dist/epmd.go +++ b/proto/dist/epmd.go @@ -120,6 +120,7 @@ func (e *epmd) handle(c net.Conn) { tcp.SetNoDelay(true) } continue + case epmdPortPleaseReq: requestedName := string(buf[3:n]) @@ -134,9 +135,11 @@ func (e *epmd) handle(c net.Conn) { } e.sendPortPleaseResp(c, requestedName, node) return + case epmdNamesReq: e.sendNamesResp(c, buf[3:n]) return + default: lib.Log("unknown EPMD request") return diff --git a/proto/dist/flusher.go b/proto/dist/flusher.go index 2ddbd308..4410bee3 100644 --- a/proto/dist/flusher.go +++ b/proto/dist/flusher.go @@ -13,12 +13,11 @@ var ( keepAlivePeriod = 15 * time.Second ) -func newLinkFlusher(w io.Writer, latency time.Duration, softwareKeepAlive bool) *linkFlusher { +func newLinkFlusher(w io.Writer, latency time.Duration) *linkFlusher { lf := &linkFlusher{ - latency: latency, - writer: bufio.NewWriter(w), - w: w, // in case if we skip buffering - softwareKeepAlive: softwareKeepAlive, + latency: latency, + writer: bufio.NewWriter(w), + w: w, // in case if we skip buffering } lf.timer = time.AfterFunc(keepAlivePeriod, func() { @@ -28,7 +27,7 @@ func newLinkFlusher(w io.Writer, latency time.Duration, softwareKeepAlive bool) // if we have no pending data to send we should // send a KeepAlive packet - if lf.pending == false && lf.softwareKeepAlive { + if lf.pending == false { lf.w.Write(keepAlivePacket) lf.timer.Reset(keepAlivePeriod) return @@ -36,20 +35,17 @@ func newLinkFlusher(w io.Writer, latency time.Duration, softwareKeepAlive bool) lf.writer.Flush() lf.pending = false - if lf.softwareKeepAlive { - lf.timer.Reset(keepAlivePeriod) - } + lf.timer.Reset(keepAlivePeriod) }) return lf } type linkFlusher struct { - mutex sync.Mutex - latency time.Duration - writer *bufio.Writer - w io.Writer - softwareKeepAlive bool + mutex sync.Mutex + latency time.Duration + writer *bufio.Writer + w io.Writer timer *time.Timer pending bool diff --git a/proto/dist/handshake.go b/proto/dist/handshake.go index 7a411a4f..54d9ca67 100644 --- a/proto/dist/handshake.go +++ b/proto/dist/handshake.go @@ -5,8 +5,8 @@ import ( "crypto/md5" "encoding/binary" "fmt" - "io" "math/rand" + "net" "time" "github.com/ergo-services/ergo/lib" @@ -122,7 +122,7 @@ func (dh *DistHandshake) Version() node.HandshakeVersion { return dh.options.Version } -func (dh *DistHandshake) Start(conn io.ReadWriter, tls bool, cookie string) (node.HandshakeDetails, error) { +func (dh *DistHandshake) Start(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (node.HandshakeDetails, error) { var details node.HandshakeDetails @@ -185,7 +185,7 @@ func (dh *DistHandshake) Start(conn io.ReadWriter, tls bool, cookie string) (nod return details, fmt.Errorf("malformed handshake (wrong packet length)") } - // chech if we got correct message type regarding to 'await' value + // check if we got correct message type regarding to 'await' value if bytes.Count(await, buffer[0:1]) == 0 { return details, fmt.Errorf("malformed handshake (wrong response)") } @@ -282,7 +282,7 @@ func (dh *DistHandshake) Start(conn io.ReadWriter, tls bool, cookie string) (nod } -func (dh *DistHandshake) Accept(conn io.ReadWriter, tls bool, cookie string) (node.HandshakeDetails, error) { +func (dh *DistHandshake) Accept(remote net.Addr, conn lib.NetReadWriter, tls bool, cookie string) (node.HandshakeDetails, error) { var details node.HandshakeDetails b := lib.TakeBuffer() diff --git a/proto/dist/proto.go b/proto/dist/proto.go index 7f42022d..0294453a 100644 --- a/proto/dist/proto.go +++ b/proto/dist/proto.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "math/rand" + "net" "sync" "sync/atomic" "time" @@ -98,7 +99,7 @@ type distConnection struct { creation uint32 // socket - conn io.ReadWriter + conn lib.NetReadWriter cancelContext context.CancelFunc // proxy session (endpoints) @@ -120,6 +121,8 @@ type distConnection struct { // atom cache for outgoing messages cache etf.AtomCache + mapping etf.AtomMapping + // fragmentation sequence ID sequenceID int64 fragments map[uint64]*fragmentedPacket @@ -130,6 +133,9 @@ type distConnection struct { checkCleanTimer *time.Timer checkCleanTimeout time.Duration // default is 5 seconds checkCleanDeadline time.Duration // how long we wait for the next fragment of the certain sequenceID. Default is 30 seconds + + // stats + stats node.NetworkStats } type distProto struct { @@ -175,7 +181,7 @@ type receivers struct { i int32 } -func (dp *distProto) Init(ctx context.Context, conn io.ReadWriter, nodename string, details node.HandshakeDetails) (node.ConnectionInterface, error) { +func (dp *distProto) Init(ctx context.Context, conn lib.NetReadWriter, nodename string, details node.HandshakeDetails) (node.ConnectionInterface, error) { connection := &distConnection{ nodename: nodename, peername: details.Name, @@ -183,6 +189,7 @@ func (dp *distProto) Init(ctx context.Context, conn io.ReadWriter, nodename stri creation: details.Creation, conn: conn, cache: etf.NewAtomCache(), + mapping: details.AtomMapping, proxySessionsByID: make(map[string]proxySession), proxySessionsByPeerName: make(map[string]proxySession), fragments: make(map[uint64]*fragmentedPacket), @@ -191,17 +198,24 @@ func (dp *distProto) Init(ctx context.Context, conn io.ReadWriter, nodename stri } connection.ctx, connection.cancelContext = context.WithCancel(ctx) + connection.stats.NodeName = details.Name + // create connection buffering - connection.flusher = newLinkFlusher(conn, defaultLatency, details.Flags.EnableSoftwareKeepAlive) + connection.flusher = newLinkFlusher(conn, defaultLatency) + + numHandlers := dp.options.NumHandlers + if details.NumHandlers > 0 { + numHandlers = details.NumHandlers + } // do not use shared channels within intencive code parts, impacts on a performance connection.receivers = receivers{ - recv: make([]chan *lib.Buffer, dp.options.NumHandlers), - n: int32(dp.options.NumHandlers), + recv: make([]chan *lib.Buffer, numHandlers), + n: int32(numHandlers), } // run readers for incoming messages - for i := 0; i < dp.options.NumHandlers; i++ { + for i := 0; i < numHandlers; i++ { // run packet reader routines (decoder) recv := make(chan *lib.Buffer, dp.options.RecvQueueLength) connection.receivers.recv[i] = recv @@ -209,12 +223,12 @@ func (dp *distProto) Init(ctx context.Context, conn io.ReadWriter, nodename stri } connection.senders = senders{ - sender: make([]*senderChannel, dp.options.NumHandlers), - n: int32(dp.options.NumHandlers), + sender: make([]*senderChannel, numHandlers), + n: int32(numHandlers), } // run readers/writers for incoming/outgoing messages - for i := 0; i < dp.options.NumHandlers; i++ { + for i := 0; i < numHandlers; i++ { // run writer routines (encoder) send := make(chan *sendMessage, dp.options.SendQueueLength) connection.senders.sender[i] = &senderChannel{ @@ -333,7 +347,7 @@ func (dc *distConnection) SendReg(from gen.Process, to gen.ProcessID, message et } func (dc *distConnection) SendAlias(from gen.Process, to etf.Alias, message etf.Term) error { if dc.flags.EnableAlias == false { - return node.ErrUnsupported + return lib.ErrUnsupported } msg := sendMessages.Get().(*sendMessage) @@ -352,7 +366,7 @@ func (dc *distConnection) Link(local etf.Pid, remote etf.Pid) error { ps, isProxy := dc.proxySessionsByPeerName[string(remote.Node)] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableLink == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoLINK, local, remote}, @@ -364,7 +378,7 @@ func (dc *distConnection) Unlink(local etf.Pid, remote etf.Pid) error { ps, isProxy := dc.proxySessionsByPeerName[string(remote.Node)] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableLink == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoUNLINK, local, remote}, @@ -383,7 +397,7 @@ func (dc *distConnection) Monitor(local etf.Pid, remote etf.Pid, ref etf.Ref) er ps, isProxy := dc.proxySessionsByPeerName[string(remote.Node)] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableMonitor == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoMONITOR, local, remote, ref}, @@ -395,7 +409,7 @@ func (dc *distConnection) MonitorReg(local etf.Pid, remote gen.ProcessID, ref et ps, isProxy := dc.proxySessionsByPeerName[remote.Node] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableMonitor == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoMONITOR, local, etf.Atom(remote.Name), ref}, @@ -407,7 +421,7 @@ func (dc *distConnection) Demonitor(local etf.Pid, remote etf.Pid, ref etf.Ref) ps, isProxy := dc.proxySessionsByPeerName[string(remote.Node)] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableMonitor == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoDEMONITOR, local, remote, ref}, @@ -419,7 +433,7 @@ func (dc *distConnection) DemonitorReg(local etf.Pid, remote gen.ProcessID, ref ps, isProxy := dc.proxySessionsByPeerName[remote.Node] dc.proxySessionsMutex.RUnlock() if isProxy && ps.session.PeerFlags.EnableMonitor == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ control: etf.Tuple{distProtoDEMONITOR, local, etf.Atom(remote.Name), ref}, @@ -445,11 +459,11 @@ func (dc *distConnection) SpawnRequest(nodeName string, behaviorName string, req dc.proxySessionsMutex.RUnlock() if isProxy { if ps.session.PeerFlags.EnableRemoteSpawn == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } } else { if dc.flags.EnableRemoteSpawn == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } } @@ -485,7 +499,7 @@ func (dc *distConnection) SpawnReplyError(to etf.Pid, ref etf.Ref, err error) er func (dc *distConnection) ProxyConnectRequest(request node.ProxyConnectRequest) error { if dc.flags.EnableProxy == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } path := []etf.Atom{} @@ -510,7 +524,7 @@ func (dc *distConnection) ProxyConnectRequest(request node.ProxyConnectRequest) func (dc *distConnection) ProxyConnectReply(reply node.ProxyConnectReply) error { if dc.flags.EnableProxy == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } path := etf.List{} @@ -536,7 +550,7 @@ func (dc *distConnection) ProxyConnectReply(reply node.ProxyConnectReply) error func (dc *distConnection) ProxyConnectCancel(err node.ProxyConnectCancel) error { if dc.flags.EnableProxy == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } path := etf.List{} @@ -558,7 +572,7 @@ func (dc *distConnection) ProxyConnectCancel(err node.ProxyConnectCancel) error func (dc *distConnection) ProxyDisconnect(disconnect node.ProxyDisconnect) error { if dc.flags.EnableProxy == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ @@ -578,11 +592,11 @@ func (dc *distConnection) ProxyRegisterSession(session node.ProxySession) error defer dc.proxySessionsMutex.Unlock() _, exist := dc.proxySessionsByPeerName[session.PeerName] if exist { - return node.ErrProxySessionDuplicate + return lib.ErrProxySessionDuplicate } _, exist = dc.proxySessionsByID[session.ID] if exist { - return node.ErrProxySessionDuplicate + return lib.ErrProxySessionDuplicate } ps := proxySession{ session: session, @@ -600,7 +614,7 @@ func (dc *distConnection) ProxyUnregisterSession(id string) error { defer dc.proxySessionsMutex.Unlock() ps, exist := dc.proxySessionsByID[id] if exist == false { - return node.ErrProxySessionUnknown + return lib.ErrProxySessionUnknown } delete(dc.proxySessionsByPeerName, ps.session.PeerName) delete(dc.proxySessionsByID, ps.session.ID) @@ -609,7 +623,7 @@ func (dc *distConnection) ProxyUnregisterSession(id string) error { func (dc *distConnection) ProxyPacket(packet *lib.Buffer) error { if dc.flags.EnableProxy == false { - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } msg := &sendMessage{ packet: packet, @@ -626,8 +640,18 @@ func (dc *distConnection) read(b *lib.Buffer, max int) (int, error) { expectingBytes := 4 for { if b.Len() < expectingBytes { + // if no data is received during the 4 * keepAlivePeriod the remote node + // seems to be stuck. + deadline := true + if err := dc.conn.SetReadDeadline(time.Now().Add(4 * keepAlivePeriod)); err != nil { + deadline = false + } + n, e := b.ReadDataFrom(dc.conn, max) if n == 0 { + if err, ok := e.(net.Error); deadline && ok && err.Timeout() { + lib.Warning("Node %q not responding. Drop connection", dc.peername) + } // link was closed return 0, nil } @@ -782,6 +806,8 @@ func (dc *distConnection) receiver(recv <-chan *lib.Buffer) { dc.ProxyDisconnect(disconnect) } + atomic.AddUint64(&dc.stats.MessagesIn, 1) + // we have to release this buffer lib.ReleaseBuffer(b) @@ -802,6 +828,8 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { if control == nil { return nil, err } + atomic.AddUint64(&dc.stats.BytesIn, uint64(b.Len())) + message := &distMessage{control: control, payload: payload} return message, err @@ -821,9 +849,12 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { Reason: err.Error(), } dc.ProxyDisconnect(disconnect) + return nil, nil } + atomic.AddUint64(&dc.stats.TransitBytesIn, uint64(b.Len())) return nil, nil } + // this node is endpoint of this session packet = b.B[37:] control, payload, err := dc.decodeDist(packet, &ps) @@ -851,6 +882,9 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { dc.ProxyDisconnect(disconnect) return nil, nil } + + atomic.AddUint64(&dc.stats.BytesIn, uint64(b.Len())) + if control == nil { return nil, nil } @@ -858,6 +892,7 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { return message, nil case protoProxyX: + atomic.AddUint64(&dc.stats.BytesIn, uint64(b.Len())) sessionID := string(packet[5:37]) dc.proxySessionsMutex.RLock() ps, exist := dc.proxySessionsByID[sessionID] @@ -873,7 +908,9 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { Reason: err.Error(), } dc.ProxyDisconnect(disconnect) + return nil, nil } + atomic.AddUint64(&dc.stats.TransitBytesIn, uint64(b.Len())) return nil, nil } @@ -890,6 +927,8 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { dc.ProxyDisconnect(disconnect) return nil, nil } + atomic.AddUint64(&dc.stats.BytesIn, uint64(b.Len())) + iv := packet[:aes.BlockSize] msg := packet[aes.BlockSize:] cfb := cipher.NewCFBDecrypter(ps.session.Block, iv) @@ -915,7 +954,6 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { if err != nil { if err == errMissingInCache { // will be deferred - // TODO make sure if this shift is correct b.B = b.B[32+aes.BlockSize:] b.B[4] = protoDist return nil, err @@ -931,6 +969,7 @@ func (dc *distConnection) decodePacket(b *lib.Buffer) (*distMessage, error) { dc.ProxyDisconnect(disconnect) return nil, nil } + atomic.AddUint64(&dc.stats.BytesIn, uint64(b.Len())) if control == nil { return nil, nil } @@ -957,6 +996,7 @@ func (dc *distConnection) decodeDist(packet []byte, proxy *proxySession) (etf.Te } decodeOptions := etf.DecodeOptions{ + AtomMapping: dc.mapping, FlagBigPidRef: dc.flags.EnableBigPidRef, } if proxy != nil { @@ -1119,7 +1159,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { if message.proxy != nil && message.proxy.session.NodeFlags.EnableLink == false { // we didn't allow this feature. proxy session will be closed due to // this violation of the contract - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } dc.router.RouteLink(t.Element(2).(etf.Pid), t.Element(3).(etf.Pid)) return nil @@ -1130,7 +1170,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { if message.proxy != nil && message.proxy.session.NodeFlags.EnableLink == false { // we didn't allow this feature. proxy session will be closed due to // this violation of the contract - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } dc.router.RouteUnlink(t.Element(2).(etf.Pid), t.Element(3).(etf.Pid)) return nil @@ -1159,7 +1199,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { if message.proxy != nil && message.proxy.session.NodeFlags.EnableMonitor == false { // we didn't allow this feature. proxy session will be closed due to // this violation of the contract - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } fromPid := t.Element(2).(etf.Pid) @@ -1189,7 +1229,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { if message.proxy != nil && message.proxy.session.NodeFlags.EnableMonitor == false { // we didn't allow this feature. proxy session will be closed due to // this violation of the contract - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } ref := t.Element(4).(etf.Ref) fromPid := t.Element(2).(etf.Pid) @@ -1244,7 +1284,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { if message.proxy != nil && message.proxy.session.NodeFlags.EnableRemoteSpawn == false { // we didn't allow this feature. proxy session will be closed due to // this violation of the contract - return node.ErrPeerUnsupported + return lib.ErrPeerUnsupported } registerName := "" for _, option := range t.Element(6).(etf.List) { @@ -1349,7 +1389,7 @@ func (dc *distConnection) handleMessage(message *distMessage) (err error) { Reason: err.Error(), } dc.ProxyDisconnect(disconnect) - if err == node.ErrNoRoute { + if err == lib.ErrNoRoute { return nil } @@ -1732,6 +1772,7 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option message := &sendMessage{} encodingOptions := etf.EncodeOptions{ EncodingAtomCache: encodingAtomCache, + AtomMapping: dc.mapping, NodeName: dc.nodename, PeerName: dc.peername, } @@ -1755,13 +1796,17 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option if message.packet != nil { // transit proxy message - if _, err := dc.flusher.Write(message.packet.B); err != nil { + bytesOut, err := dc.flusher.Write(message.packet.B) + if err != nil { return } + atomic.AddUint64(&dc.stats.TransitBytesOut, uint64(bytesOut)) lib.ReleaseBuffer(message.packet) continue } + atomic.AddUint64(&dc.stats.MessagesOut, 1) + packetBuffer = lib.TakeBuffer() lenMessage, lenAtomCache, lenPacket = 0, 0, 0 startDataPosition = reserveHeaderAtomCache @@ -1921,9 +1966,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition:], uint32(lenPacket)) packetBuffer.B[startDataPosition+4] = protoDist // 131 - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition:]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition:]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) break } @@ -1936,9 +1983,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition:], uint32(l)) packetBuffer.B[startDataPosition+4] = protoProxy copy(packetBuffer.B[startDataPosition+5:], message.proxy.session.ID) - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition:]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition:]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) break } @@ -1949,9 +1998,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option // can't encrypt message return } - if _, err := dc.flusher.Write(xBuffer.B); err != nil { + bytesOut, err := dc.flusher.Write(xBuffer.B) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) lib.ReleaseBuffer(xBuffer) break } @@ -1982,9 +2033,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option if message.proxy == nil { binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition:], uint32(lenPacket)) packetBuffer.B[startDataPosition+4] = protoDist // 131 - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition : startDataPosition+4+lenPacket]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition : startDataPosition+4+lenPacket]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) } else { // proxy message if encryptionEnabled == false { @@ -1993,9 +2046,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition-32:], uint32(lenPacket+32)) packetBuffer.B[startDataPosition-32+4] = protoProxy // 141 copy(packetBuffer.B[startDataPosition-32+5:], message.proxy.session.ID) - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition-32 : startDataPosition+4+lenPacket]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition-32 : startDataPosition+4+lenPacket]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) } else { // send encrypted proxy message @@ -2008,9 +2063,12 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option // can't encrypt message return } - if _, err := dc.flusher.Write(xBuffer.B); err != nil { + bytesOut, err := dc.flusher.Write(xBuffer.B) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) + // resore tail copy(packetBuffer.B[startDataPosition+4+lenPacket:], tail16[:n]) lib.ReleaseBuffer(xBuffer) @@ -2045,18 +2103,22 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option // send fragment binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition:], uint32(lenPacket)) packetBuffer.B[startDataPosition+4] = protoDist // 131 - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition : startDataPosition+4+lenPacket]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition : startDataPosition+4+lenPacket]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) } else { // wrap it as a proxy message if encryptionEnabled == false { binary.BigEndian.PutUint32(packetBuffer.B[startDataPosition-32:], uint32(lenPacket+32)) packetBuffer.B[startDataPosition-32+4] = protoProxy // 141 copy(packetBuffer.B[startDataPosition-32+5:], message.proxy.session.ID) - if _, err := dc.flusher.Write(packetBuffer.B[startDataPosition-32 : startDataPosition+4+lenPacket]); err != nil { + bytesOut, err := dc.flusher.Write(packetBuffer.B[startDataPosition-32 : startDataPosition+4+lenPacket]) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) } else { // send encrypted proxy message tail16 := [16]byte{} @@ -2067,9 +2129,11 @@ func (dc *distConnection) sender(sender_id int, send <-chan *sendMessage, option // can't encrypt message return } - if _, err := dc.flusher.Write(xBuffer.B); err != nil { + bytesOut, err := dc.flusher.Write(xBuffer.B) + if err != nil { return } + atomic.AddUint64(&dc.stats.BytesOut, uint64(bytesOut)) // resore tail copy(packetBuffer.B[startDataPosition+4+lenPacket:], tail16[:n]) lib.ReleaseBuffer(xBuffer) @@ -2118,7 +2182,7 @@ func (dc *distConnection) send(to string, creation uint32, msg *sendMessage) err s := dc.senders.sender[n] if s == nil { // connection was closed - return node.ErrNoRoute + return lib.ErrNoRoute } dc.proxySessionsMutex.RLock() ps, isProxy := dc.proxySessionsByPeerName[to] @@ -2137,7 +2201,7 @@ func (dc *distConnection) send(to string, creation uint32, msg *sendMessage) err // if this peer is Erlang OTP 22 (and earlier), peer_creation is always 0, so we // must skip this checking. if creation > 0 && peer_creation > 0 && peer_creation != creation { - return node.ErrProcessIncarnation + return lib.ErrProcessIncarnation } // TODO to decide whether to return error if channel is full @@ -2155,6 +2219,10 @@ func (dc *distConnection) send(to string, creation uint32, msg *sendMessage) err return nil } +func (dc *distConnection) Stats() node.NetworkStats { + return dc.stats +} + func proxyFlagsToUint64(pf node.ProxyFlags) uint64 { var flags uint64 if pf.EnableLink { diff --git a/proto/dist/resolver.go b/proto/dist/registrar.go similarity index 76% rename from proto/dist/resolver.go rename to proto/dist/registrar.go index 4e5be888..3bafeee2 100644 --- a/proto/dist/resolver.go +++ b/proto/dist/registrar.go @@ -2,6 +2,7 @@ package dist import ( "context" + "crypto/tls" "encoding/binary" "fmt" "io" @@ -34,10 +35,8 @@ const ( ergoExtraVersion1 = 1 ) -// epmd implements resolver -type epmdResolver struct { - node.Resolver - +// epmd implements registrar interface +type epmdRegistrar struct { // EPMD server enableEPMD bool host string @@ -53,37 +52,37 @@ type epmdResolver struct { extra []byte } -func CreateResolver() node.Resolver { - resolver := &epmdResolver{ +func CreateRegistrar() node.Registrar { + registrar := &epmdRegistrar{ port: DefaultEPMDPort, } - return resolver + return registrar } -func CreateResolverWithLocalEPMD(host string, port uint16) node.Resolver { +func CreateRegistrarWithLocalEPMD(host string, port uint16) node.Registrar { if port == 0 { port = DefaultEPMDPort } - resolver := &epmdResolver{ + registrar := &epmdRegistrar{ enableEPMD: true, host: host, port: port, } - return resolver + return registrar } -func CreateResolverWithRemoteEPMD(host string, port uint16) node.Resolver { +func CreateRegistrarWithRemoteEPMD(host string, port uint16) node.Registrar { if port == 0 { port = DefaultEPMDPort } - resolver := &epmdResolver{ + registrar := &epmdRegistrar{ host: host, port: port, } - return resolver + return registrar } -func (e *epmdResolver) Register(ctx context.Context, name string, port uint16, options node.ResolverOptions) error { +func (e *epmdRegistrar) Register(ctx context.Context, name string, options node.RegisterOptions) error { n := strings.Split(name, "@") if len(n) != 2 { return fmt.Errorf("(EMPD) FQDN for node name is required (example: node@hostname)") @@ -92,7 +91,7 @@ func (e *epmdResolver) Register(ctx context.Context, name string, port uint16, o e.name = name e.nodeName = n[0] e.nodeHost = n[1] - e.nodePort = port + e.nodePort = options.Port e.handshakeVersion = options.HandshakeVersion e.composeExtra(options) @@ -150,7 +149,7 @@ func (e *epmdResolver) Register(ctx context.Context, name string, port uint16, o return <-ready } -func (e *epmdResolver) Resolve(name string) (node.Route, error) { +func (e *epmdRegistrar) Resolve(name string) (node.Route, error) { var route node.Route n := strings.Split(name, "@") @@ -177,10 +176,24 @@ func (e *epmdResolver) Resolve(name string) (node.Route, error) { } return route, nil +} +func (e *epmdRegistrar) ResolveProxy(name string) (node.ProxyRoute, error) { + var route node.ProxyRoute + return route, lib.ErrProxyNoRoute +} +func (e *epmdRegistrar) RegisterProxy(name string, maxhop int, flags node.ProxyFlags) error { + return lib.ErrUnsupported +} +func (e *epmdRegistrar) UnregisterProxy(name string) error { + return lib.ErrUnsupported +} +func (e *epmdRegistrar) Config() (node.RegistrarConfig, error) { + var cfg node.RegistrarConfig + return cfg, lib.ErrUnsupported } -func (e *epmdResolver) composeExtra(options node.ResolverOptions) { +func (e *epmdRegistrar) composeExtra(options node.RegisterOptions) { buf := make([]byte, 4) // 2 bytes: ergoExtraMagic @@ -195,7 +208,7 @@ func (e *epmdResolver) composeExtra(options node.ResolverOptions) { return } -func (e *epmdResolver) readExtra(route *node.Route, buf []byte) { +func (e *epmdRegistrar) readExtra(route *node.Route, buf []byte) { if len(buf) < 4 { return } @@ -209,23 +222,23 @@ func (e *epmdResolver) readExtra(route *node.Route, buf []byte) { } if buf[3] == 1 { - route.Options.EnableTLS = true + route.Options.TLS = &tls.Config{} } route.Options.IsErgo = true return } -func (e *epmdResolver) registerNode(options node.ResolverOptions) (net.Conn, error) { +func (e *epmdRegistrar) registerNode(options node.RegisterOptions) (net.Conn, error) { // - resolverHost := e.host - if resolverHost == "" { - resolverHost = e.nodeHost + registrarHost := e.host + if registrarHost == "" { + registrarHost = e.nodeHost } dialer := net.Dialer{ KeepAlive: 15 * time.Second, } - dsn := net.JoinHostPort(resolverHost, strconv.Itoa(int(e.port))) + dsn := net.JoinHostPort(registrarHost, strconv.Itoa(int(e.port))) conn, err := dialer.Dial("tcp", dsn) if err != nil { return nil, err @@ -245,7 +258,7 @@ func (e *epmdResolver) registerNode(options node.ResolverOptions) (net.Conn, err return conn, nil } -func (e *epmdResolver) sendAliveReq(conn net.Conn) error { +func (e *epmdRegistrar) sendAliveReq(conn net.Conn) error { buf := make([]byte, 2+14+len(e.nodeName)+len(e.extra)) binary.BigEndian.PutUint16(buf[0:2], uint16(len(buf)-2)) buf[2] = byte(epmdAliveReq) @@ -277,7 +290,7 @@ func (e *epmdResolver) sendAliveReq(conn net.Conn) error { return nil } -func (e *epmdResolver) readAliveResp(conn net.Conn) error { +func (e *epmdRegistrar) readAliveResp(conn net.Conn) error { buf := make([]byte, 16) if _, err := conn.Read(buf); err != nil { return err @@ -296,7 +309,7 @@ func (e *epmdResolver) readAliveResp(conn net.Conn) error { return nil } -func (e *epmdResolver) sendPortPleaseReq(conn net.Conn, name string) error { +func (e *epmdRegistrar) sendPortPleaseReq(conn net.Conn, name string) error { buflen := uint16(2 + len(name) + 1) buf := make([]byte, buflen) binary.BigEndian.PutUint16(buf[0:2], uint16(len(buf)-2)) @@ -306,7 +319,7 @@ func (e *epmdResolver) sendPortPleaseReq(conn net.Conn, name string) error { return err } -func (e *epmdResolver) readPortResp(route *node.Route, c net.Conn) error { +func (e *epmdRegistrar) readPortResp(route *node.Route, c net.Conn) error { buf := make([]byte, 1024) n, err := c.Read(buf) diff --git a/tests/application_test.go b/tests/application_test.go index a3d5d814..7880f0ac 100644 --- a/tests/application_test.go +++ b/tests/application_test.go @@ -9,6 +9,7 @@ import ( "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -27,7 +28,7 @@ func (a *testApplication) Load(args ...etf.Term) (gen.ApplicationSpec, error) { Name: name, Description: "My Test Applicatoin", Version: "v.0.1", - Environment: map[gen.EnvKey]interface{}{ + Env: map[gen.EnvKey]interface{}{ "envName1": 123, "envName2": "Hello world", }, @@ -89,15 +90,15 @@ func TestApplicationBasics(t *testing.T) { la := mynode.LoadedApplications() - // there is yet another default application - KernelApp. thats why it - // should be equal 2. - if len(la) != 2 { + // there are default applications - KernelApp, SystemApp thats why it + // should be equal 3. + if len(la) != 3 { t.Fatal("total number of loaded application mismatch") } fmt.Println("OK") wa := mynode.WhichApplications() - if len(wa) > 1 { + if len(wa) > 2 { t.Fatal("total number of running application mismatch") } @@ -106,7 +107,7 @@ func TestApplicationBasics(t *testing.T) { t.Fatal(err) } la = mynode.LoadedApplications() - if len(la) > 1 { + if len(la) > 2 { t.Fatal("total number of loaded application mismatch") } fmt.Println("OK") @@ -126,14 +127,14 @@ func TestApplicationBasics(t *testing.T) { fmt.Println("OK") fmt.Printf("... try to unload started application (shouldn't be able): ") - if e := mynode.ApplicationUnload("testapp1"); e != node.ErrAppAlreadyStarted { + if e := mynode.ApplicationUnload("testapp1"); e != lib.ErrAppAlreadyStarted { t.Fatal(e) } fmt.Println("OK") - fmt.Printf("... check total number of running applications (should be 2 including KernelApp): ") + fmt.Printf("... check total number of running applications (should be 3 including KernelApp, SystemApp): ") wa = mynode.WhichApplications() - if n := len(wa); n != 2 { + if n := len(wa); n != 3 { t.Fatal(n) } fmt.Println("OK") @@ -215,7 +216,7 @@ func TestApplicationBasics(t *testing.T) { t.Fatal(e) } wa = mynode.WhichApplications() - if len(wa) != 1 { + if len(wa) != 2 { fmt.Println("waa: ", wa) t.Fatal("total number of running application mismatch") } @@ -371,7 +372,7 @@ func TestApplicationTypeTransient(t *testing.T) { t.Fatal(e) } - if e := p1.WaitWithTimeout(100 * time.Millisecond); e != node.ErrTimeout { + if e := p1.WaitWithTimeout(100 * time.Millisecond); e != lib.ErrTimeout { t.Fatal("application testapp1 should be alive here") } @@ -455,7 +456,7 @@ func TestApplicationTypeTemporary(t *testing.T) { t.Fatal(e) } - if e := mynode.WaitWithTimeout(100 * time.Millisecond); e != node.ErrTimeout { + if e := mynode.WaitWithTimeout(100 * time.Millisecond); e != lib.ErrTimeout { t.Fatal("node should be alive here") } diff --git a/tests/atomcache_test.go b/tests/atomcache_test.go index 8e383f47..dd5f3882 100644 --- a/tests/atomcache_test.go +++ b/tests/atomcache_test.go @@ -464,6 +464,7 @@ func TestAtomCacheLess255UniqViaProxy(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2Less255ViaProxy@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2Less255ViaProxy@localhost", "cookie", opts2) if e != nil { @@ -476,9 +477,10 @@ func TestAtomCacheLess255UniqViaProxy(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) @@ -596,6 +598,7 @@ func TestAtomCacheMore255UniqViaProxy(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2More255ViaProxy@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2More255ViaProxy@localhost", "cookie", opts2) if e != nil { @@ -608,9 +611,10 @@ func TestAtomCacheMore255UniqViaProxy(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) @@ -732,6 +736,7 @@ func TestAtomCacheLess255UniqViaProxyWithEncryption(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2Less255ViaProxyEnc@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2Less255ViaProxyEnc@localhost", "cookie", opts2) if e != nil { @@ -744,9 +749,10 @@ func TestAtomCacheLess255UniqViaProxyWithEncryption(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) @@ -868,6 +874,7 @@ func TestAtomCacheMore255UniqViaProxyWithEncryption(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2More255ViaProxyEnc@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2More255ViaProxyEnc@localhost", "cookie", opts2) if e != nil { @@ -880,9 +887,10 @@ func TestAtomCacheMore255UniqViaProxyWithEncryption(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) @@ -1004,6 +1012,7 @@ func TestAtomCacheLess255UniqViaProxyWithEncryptionCompression(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2Less255ViaProxyEncComp@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2Less255ViaProxyEncComp@localhost", "cookie", opts2) if e != nil { @@ -1016,9 +1025,10 @@ func TestAtomCacheLess255UniqViaProxyWithEncryptionCompression(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) @@ -1142,6 +1152,7 @@ func TestAtomCacheMore255UniqViaProxyWithEncryptionCompression(t *testing.T) { fmt.Printf("Starting node: nodeAtomCache2More255ViaProxyEncComp@localhost with NubHandlers = 2: ") opts2 := node.Options{} + opts2.Proxy.Accept = true opts2.Proto = dist.CreateProto(protoOptions) node2, e := ergo.StartNode("nodeAtomCache2More255ViaProxyEncComp@localhost", "cookie", opts2) if e != nil { @@ -1154,9 +1165,10 @@ func TestAtomCacheMore255UniqViaProxyWithEncryptionCompression(t *testing.T) { fmt.Printf(" connect %s with %s via proxy %s: ", node1.Name(), node2.Name(), nodeT.Name()) route := node.ProxyRoute{ + Name: node2.Name(), Proxy: nodeT.Name(), } - node1.AddProxyRoute(node2.Name(), route) + node1.AddProxyRoute(route) if err := node1.Connect(node2.Name()); err != nil { t.Fatal(err) diff --git a/tests/core_test.go b/tests/core_test.go index a22da074..fae8f7bb 100644 --- a/tests/core_test.go +++ b/tests/core_test.go @@ -8,6 +8,7 @@ import ( "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -20,12 +21,12 @@ func (trg *TestCoreGenserver) HandleCall(process *gen.ServerProcess, from gen.Se return message, gen.ServerStatusOK } -func (trg *TestCoreGenserver) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (trg *TestCoreGenserver) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func TestCore(t *testing.T) { @@ -108,7 +109,7 @@ func TestCore(t *testing.T) { fmt.Println("OK") fmt.Printf("...try to unregister 'test' related to %v using gs2 process (not allowed): ", node1gs1.Self()) - if err := node1gs2.UnregisterName("test"); err != node.ErrNameOwner { + if err := node1gs2.UnregisterName("test"); err != lib.ErrNameOwner { t.Fatal("not allowed to unregister by not an owner") } fmt.Println("OK") @@ -177,7 +178,7 @@ func TestCoreAlias(t *testing.T) { fmt.Println("OK") fmt.Printf(" Delete gs1 alias by gs2 (not allowed): ") - if err := node1gs2.DeleteAlias(alias); err != node.ErrAliasOwner { + if err := node1gs2.DeleteAlias(alias); err != lib.ErrAliasOwner { t.Fatal(" expected ErrAliasOwner, got:", err) } fmt.Println("OK") @@ -215,7 +216,7 @@ func TestCoreAlias(t *testing.T) { fmt.Printf(" Create gs1 alias on a stopped process (shouldn't be allowed): ") alias, err = node1gs1.CreateAlias() - if err != node.ErrProcessTerminated { + if err != lib.ErrProcessTerminated { t.Fatal(err) } fmt.Println("OK") diff --git a/tests/monitor_test.go b/tests/monitor_test.go index c1d3cec0..4d432575 100644 --- a/tests/monitor_test.go +++ b/tests/monitor_test.go @@ -9,6 +9,7 @@ import ( "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -430,15 +431,18 @@ func TestMonitorLocalProxyRemoteByPid(t *testing.T) { if err != nil { t.Fatal("can't start node:", err, node2.Name()) } - node3, err := ergo.StartNode("nodeM3ProxyRemoteByPid@localhost", "cookies", node.Options{}) + opts3 := node.Options{} + opts3.Proxy.Accept = true + node3, err := ergo.StartNode("nodeM3ProxyRemoteByPid@localhost", "cookies", opts3) if err != nil { t.Fatal("can't start node:", err) } route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) node1.Connect(node3.Name()) fmt.Println("OK") @@ -588,14 +592,17 @@ func TestMonitorLocalProxyRemoteByName(t *testing.T) { if err != nil { t.Fatal("can't start node:", err) } - node3, err := ergo.StartNode("nodeM3RemoteByName@localhost", "cookies", node.Options{}) + opts3 := node.Options{} + opts3.Proxy.Accept = true + node3, err := ergo.StartNode("nodeM3RemoteByName@localhost", "cookies", opts3) if err != nil { t.Fatal("can't start node:", err) } route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) node1.Connect(node3.Name()) fmt.Println("OK") @@ -1083,18 +1090,21 @@ func TestLinkRemoteProxy(t *testing.T) { if err != nil { t.Fatal(err) } - node3, err := ergo.StartNode("nodeL3RemoteViaProxy@localhost", "cookies", node.Options{}) + node3opts := node.Options{} + node3opts.Proxy.Accept = true + node3, err := ergo.StartNode("nodeL3RemoteViaProxy@localhost", "cookies", node3opts) if err != nil { t.Fatal(err) } fmt.Println("OK") route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } route.Flags = node.DefaultProxyFlags() route.Flags.EnableLink = false - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) fmt.Printf(" check connectivity of %s with %s via proxy %s: ", node1.Name(), node3.Name(), node2.Name()) if err := node1.Connect(node3.Name()); err != nil { @@ -1281,7 +1291,9 @@ func TestLinkRemoteProxy(t *testing.T) { t.Fatal("number of links has changed on the second Link call") } - node3, err = ergo.StartNode("nodeL3RemoteViaProxy@localhost", "cookies", node.Options{}) + node3opts = node.Options{} + node3opts.Proxy.Accept = true + node3, err = ergo.StartNode("nodeL3RemoteViaProxy@localhost", "cookies", node3opts) fmt.Printf(" starting node: %s", node3.Name()) if err != nil { t.Fatal(err) @@ -1298,7 +1310,7 @@ func TestLinkRemoteProxy(t *testing.T) { node3gs3.SetTrapExit(true) fmt.Printf("Testing Proxy Local-Proxy-Remote for link gs3 -> gs1 (Node1 ProxyFlags.EnableLink = false): ") node3gs3.Link(node1gs1.Self()) - result = gen.MessageExit{Pid: node1gs1.Self(), Reason: node.ErrPeerUnsupported.Error()} + result = gen.MessageExit{Pid: node1gs1.Self(), Reason: lib.ErrPeerUnsupported.Error()} waitForResultWithValue(t, gs3.v, result) node1gs1.Link(node3gs3.Self()) @@ -1346,6 +1358,7 @@ func TestMonitorNode(t *testing.T) { } optsD := node.Options{} + optsD.Proxy.Accept = true nodeD, e := ergo.StartNode("monitornodeDproxy@localhost", "secret", optsD) if e != nil { t.Fatal(e) @@ -1381,18 +1394,20 @@ func TestMonitorNode(t *testing.T) { waitForResultWithValue(t, gsD.v, pD.Self()) fmt.Printf("... add proxy route on A to the node D via B: ") routeAtoDviaB := node.ProxyRoute{ + Name: nodeD.Name(), Proxy: nodeB.Name(), } - if err := nodeA.AddProxyRoute(nodeD.Name(), routeAtoDviaB); err != nil { + if err := nodeA.AddProxyRoute(routeAtoDviaB); err != nil { t.Fatal(err) } fmt.Println("OK") fmt.Printf("... add proxy transit route on B to the node D via C: ") route := node.ProxyRoute{ + Name: nodeD.Name(), Proxy: nodeC.Name(), } - if err := nodeB.AddProxyRoute(nodeD.Name(), route); err != nil { + if err := nodeB.AddProxyRoute(route); err != nil { t.Fatal(err) } fmt.Println("OK") @@ -1479,6 +1494,299 @@ func TestMonitorNode(t *testing.T) { nodeB.Stop() } +type testMonitorEvent struct { + gen.Server + v chan interface{} +} + +type testEventCmdRegister struct { + event gen.Event + messages []gen.EventMessage +} +type testEventCmdUnregister struct { + event gen.Event +} +type testEventCmdMonitor struct { + event gen.Event +} +type testEventCmdSend struct { + event gen.Event + message gen.EventMessage +} + +type testMessageEventA struct { + value string +} + +func (tgs *testMonitorEvent) Init(process *gen.ServerProcess, args ...etf.Term) error { + tgs.v <- process.Self() + return nil +} +func (tgs *testMonitorEvent) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { + switch cmd := message.(type) { + case testEventCmdRegister: + return nil, process.RegisterEvent(cmd.event, cmd.messages...) + case testEventCmdUnregister: + return nil, process.UnregisterEvent(cmd.event) + case testEventCmdMonitor: + return nil, process.MonitorEvent(cmd.event) + case testEventCmdSend: + return nil, process.SendEventMessage(cmd.event, cmd.message) + + default: + return nil, fmt.Errorf("unknown cmd") + + } +} + +func (tgs *testMonitorEvent) HandleInfo(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + tgs.v <- message + return gen.ServerStatusOK +} + +func TestMonitorEvents(t *testing.T) { + fmt.Printf("\n=== Test Monitor Events\n") + fmt.Printf("Starting node: nodeM1Events@localhost: ") + node1, _ := ergo.StartNode("nodeM1Events@localhost", "cookies", node.Options{}) + if node1 == nil { + t.Fatal("can't start node") + } + defer node1.Stop() + + fmt.Println("OK") + gs1 := &testMonitorEvent{ + v: make(chan interface{}, 2), + } + gs2 := &testMonitorEvent{ + v: make(chan interface{}, 2), + } + gs3 := &testMonitorEvent{ + v: make(chan interface{}, 2), + } + // starting gen servers + + fmt.Printf(" wait for start of gs1 on %#v: ", node1.Name()) + node1gs1, err := node1.Spawn("gs1", gen.ProcessOptions{}, gs1, nil) + if err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, gs1.v, node1gs1.Self()) + + fmt.Printf(" wait for start of gs2 on %#v: ", node1.Name()) + node1gs2, err := node1.Spawn("gs2", gen.ProcessOptions{}, gs2, nil) + if err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, gs2.v, node1gs2.Self()) + + fmt.Printf(" wait for start of gs3 on %#v: ", node1.Name()) + node1gs3, err := node1.Spawn("gs3", gen.ProcessOptions{}, gs3, nil) + if err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, gs3.v, node1gs3.Self()) + + fmt.Printf("... register new event : ") + cmd := testEventCmdRegister{ + event: "testEvent", + messages: []gen.EventMessage{testMessageEventA{}}, + } + _, err = node1gs1.Direct(cmd) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... register new event with the same name: ") + _, err = node1gs1.Direct(cmd) + if err != lib.ErrTaken { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... unregister unknown event: ") + cmd1 := testEventCmdUnregister{ + event: "unknownEvent", + } + _, err = node1gs1.Direct(cmd1) + if err != lib.ErrEventUnknown { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... unregister event by not an owner: ") + cmd1 = testEventCmdUnregister{ + event: "testEvent", + } + _, err = node1gs2.Direct(cmd1) + if err != lib.ErrEventOwner { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... unregister event by the owner: ") + cmd1 = testEventCmdUnregister{ + event: "testEvent", + } + _, err = node1gs1.Direct(cmd1) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... monitor unknown event: ") + cmd2 := testEventCmdMonitor{ + event: "testEvent", + } + _, err = node1gs2.Direct(cmd2) + if err != lib.ErrEventUnknown { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... monitor event: ") + cmd = testEventCmdRegister{ + event: "testEvent", + messages: []gen.EventMessage{testMessageEventA{}}, + } + _, err = node1gs1.Direct(cmd) + if err != nil { + t.Fatal(err) + } + + cmd2 = testEventCmdMonitor{ + event: "testEvent", + } + _, err = node1gs2.Direct(cmd2) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... monitor event itself: ") + cmd2 = testEventCmdMonitor{ + event: "testEvent", + } + _, err = node1gs1.Direct(cmd2) + if err != lib.ErrEventSelf { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... send unknown event: ") + msg := testMessageEventA{value: "test"} + cmd3 := testEventCmdSend{ + event: "unknownEvent", + message: msg, + } + _, err = node1gs1.Direct(cmd3) + if err != lib.ErrEventUnknown { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... send event with wrong message type: ") + msgWrong := "wrong type" + cmd3 = testEventCmdSend{ + event: "testEvent", + message: msgWrong, + } + _, err = node1gs1.Direct(cmd3) + if err != lib.ErrEventMismatch { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... send event by not an owner: ") + cmd3 = testEventCmdSend{ + event: "testEvent", + message: msg, + } + _, err = node1gs2.Direct(cmd3) + if err != lib.ErrEventOwner { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... send event: ") + cmd3 = testEventCmdSend{ + event: "testEvent", + message: msg, + } + _, err = node1gs1.Direct(cmd3) + if err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, gs2.v, msg) + + fmt.Printf("... monitor event twice: ") + cmd2 = testEventCmdMonitor{ + event: "testEvent", + } + _, err = node1gs2.Direct(cmd2) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("... send event. must be received twice: ") + cmd3 = testEventCmdSend{ + event: "testEvent", + message: msg, + } + _, err = node1gs1.Direct(cmd3) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + fmt.Printf("... receive first event message: ") + waitForResultWithValue(t, gs2.v, msg) + fmt.Printf("... receive second event message: ") + waitForResultWithValue(t, gs2.v, msg) + + down := gen.MessageEventDown{ + Event: "testEvent", + Reason: "unregistered", + } + fmt.Printf("... unregister event owner. must be received gen.MessageEventDown twice with reason 'unregistered': ") + cmd1 = testEventCmdUnregister{ + event: "testEvent", + } + _, err = node1gs1.Direct(cmd1) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + fmt.Printf("... receive first event down message: ") + waitForResultWithValue(t, gs2.v, down) + fmt.Printf("... receive second event down message: ") + waitForResultWithValue(t, gs2.v, down) + + cmd = testEventCmdRegister{ + event: "testEvent", + messages: []gen.EventMessage{testMessageEventA{}}, + } + _, err = node1gs3.Direct(cmd) + if err != nil { + t.Fatal(err) + } + + cmd2 = testEventCmdMonitor{ + event: "testEvent", + } + _, err = node1gs2.Direct(cmd2) + if err != nil { + t.Fatal(err) + } + fmt.Printf("... terminate event owner. must be received gen.MessageEventDown with reason 'kill': ") + node1gs3.Kill() + down = gen.MessageEventDown{ + Event: "testEvent", + Reason: "kill", + } + waitForResultWithValue(t, gs2.v, down) +} + // helpers func checkCleanProcessRef(p gen.Process, ref etf.Ref) error { if p.IsMonitor(ref) { diff --git a/tests/node_test.go b/tests/node_test.go index 53ed30ac..04a229bf 100644 --- a/tests/node_test.go +++ b/tests/node_test.go @@ -3,6 +3,7 @@ package tests import ( "context" "crypto/md5" + "crypto/tls" "fmt" "math/rand" "net" @@ -26,16 +27,19 @@ type benchCase struct { func TestNode(t *testing.T) { ctx := context.Background() + listener := node.Listener{ + Listen: 25001, + } opts := node.Options{ - Listen: 25001, - Resolver: dist.CreateResolverWithLocalEPMD("", 24999), + Listeners: []node.Listener{listener}, + Registrar: dist.CreateRegistrarWithLocalEPMD("", 24999), } - node1, _ := ergo.StartNodeWithContext(ctx, "node@localhost", "cookies", opts) + node1, _ := ergo.StartNodeWithContext(ctx, "node123@localhost", "cookies", opts) optsTaken := node.Options{ - Resolver: dist.CreateResolverWithLocalEPMD("", 24999), + Registrar: dist.CreateRegistrarWithLocalEPMD("", 24999), } - if _, err := ergo.StartNodeWithContext(ctx, "node@localhost", "cookies", optsTaken); err == nil { + if _, err := ergo.StartNodeWithContext(ctx, "node123@localhost", "cookies", optsTaken); err == nil { t.Fatal("must be failed here") } @@ -99,12 +103,21 @@ type makeCast struct { message interface{} } -func (f *testFragmentationGS) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +type asyncDirect struct { + ref etf.Ref + val etf.Term +} + +type syncDirect struct { + val etf.Term +} + +func (f *testFragmentationGS) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func TestNodeFragmentation(t *testing.T) { @@ -218,17 +231,22 @@ func (h *handshakeGenServer) Init(process *gen.ServerProcess, args ...etf.Term) func (h *handshakeGenServer) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { return "pass", gen.ServerStatusOK } -func (h *handshakeGenServer) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (h *handshakeGenServer) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func TestNodeDistHandshake(t *testing.T) { fmt.Printf("\n=== Test Node Handshake versions\n") + cert, err := lib.GenerateSelfSignedCert("localhost") + if err != nil { + t.Fatal(err) + } + // handshake version 5 handshake5options := dist.HandshakeOptions{ Version: dist.HandshakeVersion5, @@ -292,7 +310,7 @@ func TestNodeDistHandshake(t *testing.T) { } node9Options5WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake5options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node9, e9 := ergo.StartNode("node9Handshake5@localhost", "secret", node9Options5WithTLS) if e9 != nil { @@ -300,7 +318,7 @@ func TestNodeDistHandshake(t *testing.T) { } node10Options5WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake5options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node10, e10 := ergo.StartNode("node10Handshake5@localhost", "secret", node10Options5WithTLS) if e10 != nil { @@ -308,7 +326,7 @@ func TestNodeDistHandshake(t *testing.T) { } node11Options5WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake5options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node11, e11 := ergo.StartNode("node11Handshake5@localhost", "secret", node11Options5WithTLS) if e11 != nil { @@ -316,7 +334,7 @@ func TestNodeDistHandshake(t *testing.T) { } node12Options6WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake6options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node12, e12 := ergo.StartNode("node12Handshake6@localhost", "secret", node12Options6WithTLS) if e12 != nil { @@ -326,7 +344,7 @@ func TestNodeDistHandshake(t *testing.T) { // node14, _ := ergo.StartNode("node14Handshake5@localhost", "secret", nodeOptions5WithTLS) node15Options6WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake6options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node15, e15 := ergo.StartNode("node15Handshake6@localhost", "secret", node15Options6WithTLS) if e15 != nil { @@ -334,7 +352,7 @@ func TestNodeDistHandshake(t *testing.T) { } node16Options6WithTLS := node.Options{ Handshake: dist.CreateHandshake(handshake6options), - TLS: node.TLS{Enable: true}, + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, } node16, e16 := ergo.StartNode("node16Handshake6@localhost", "secret", node16Options6WithTLS) if e16 != nil { @@ -399,11 +417,14 @@ func TestNodeRemoteSpawn(t *testing.T) { node2opts := node.Options{} node2opts.Proxy.Transit = true node2, _ := ergo.StartNode("node2remoteSpawn@localhost", "secret", node2opts) - node3, _ := ergo.StartNode("node3remoteSpawn@localhost", "secret", node.Options{}) + node3opts := node.Options{} + node3opts.Proxy.Accept = true + node3, _ := ergo.StartNode("node3remoteSpawn@localhost", "secret", node3opts) route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) defer node1.Stop() defer node2.Stop() defer node3.Stop() @@ -437,13 +458,13 @@ func TestNodeRemoteSpawn(t *testing.T) { fmt.Printf(" process gs1@node1 request to spawn new process on node2 with the same name (must be failed): ") _, err = process.RemoteSpawn(node2.Name(), "remote", opts, 1, 2, 3) - if err != node.ErrTaken { + if err != lib.ErrTaken { t.Fatal(err) } fmt.Println("OK") fmt.Printf(" process gs1@node1 request to spawn new process on node2 with unregistered behavior name (must be failed): ") _, err = process.RemoteSpawn(node2.Name(), "randomname", opts, 1, 2, 3) - if err != node.ErrBehaviorUnknown { + if err != lib.ErrBehaviorUnknown { t.Fatal(err) } fmt.Println("OK") @@ -468,25 +489,33 @@ func TestNodeRemoteSpawn(t *testing.T) { t.Fatal(err) } gotPid, err = process3.RemoteSpawn(node1.Name(), "remote", opts, 1, 2, 3) - if err != node.ErrPeerUnsupported { + if err != lib.ErrPeerUnsupported { t.Fatal(err) } fmt.Println("OK") } func TestNodeResolveExtra(t *testing.T) { + cert, err := lib.GenerateSelfSignedCert("localhost") + if err != nil { + t.Fatal(err) + } fmt.Printf("\n=== Test Node Resolve Extra \n") fmt.Printf("... starting node1 with disabled TLS: ") - node1, err := ergo.StartNode("node1resolveExtra@localhost", "secret", node.Options{}) + opts1 := node.Options{ + TLS: &tls.Config{InsecureSkipVerify: true}, + } + node1, err := ergo.StartNode("node1resolveExtra@localhost", "secret", opts1) if err != nil { t.Fatal(err) } defer node1.Stop() fmt.Println("OK") - opts := node.Options{} - opts.TLS.Enable = true + opts2 := node.Options{ + TLS: &tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true}, + } fmt.Printf("... starting node2 with enabled TLS: ") - node2, err := ergo.StartNode("node2resolveExtra@localhost", "secret", opts) + node2, err := ergo.StartNode("node2resolveExtra@localhost", "secret", opts2) if err != nil { t.Fatal(err) } @@ -498,8 +527,8 @@ func TestNodeResolveExtra(t *testing.T) { if err != nil { t.Fatal(err) } - if route1.Options.EnableTLS == false { - t.Fatal("expected true value") + if route1.Options.TLS == nil { + t.Fatal("expected TLS value") } fmt.Println("OK") @@ -508,8 +537,8 @@ func TestNodeResolveExtra(t *testing.T) { if err != nil { t.Fatal(err) } - if route2.Options.EnableTLS == true { - t.Fatal("expected true value") + if route2.Options.TLS != nil { + t.Fatal("expected nil value for TLS") } fmt.Println("OK") @@ -625,12 +654,12 @@ func (c *compressionServer) HandleCall(process *gen.ServerProcess, from gen.Serv } return result, gen.ServerStatusOK } -func (c *compressionServer) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (c *compressionServer) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func TestNodeCompression(t *testing.T) { fmt.Printf("\n=== Test Node Compression \n") @@ -715,10 +744,13 @@ func TestNodeProxyConnect(t *testing.T) { if e != nil { t.Fatal(e) } + defer nodeA.Stop() + route := node.ProxyRoute{ + Name: "nodeCproxy@localhost", Proxy: "nodeBproxy@localhost", } - nodeA.AddProxyRoute("nodeCproxy@localhost", route) + nodeA.AddProxyRoute(route) optsB := node.Options{} optsB.Proxy.Transit = true @@ -726,11 +758,14 @@ func TestNodeProxyConnect(t *testing.T) { if e != nil { t.Fatal(e) } + defer nodeB.Stop() optsC := node.Options{} + optsC.Proxy.Accept = true nodeC, e := ergo.StartNode("nodeCproxy@localhost", "secret", optsC) if e != nil { t.Fatal(e) } + defer nodeC.Stop() if err := nodeA.Connect("nodeCproxy@localhost"); err != nil { t.Fatal(err) @@ -812,6 +847,7 @@ func TestNodeProxyConnect(t *testing.T) { optsC = node.Options{} optsC.Proxy.Cookie = "123" + optsC.Proxy.Accept = true nodeC, e = ergo.StartNode("nodeCproxy@localhost", "secret", optsC) if e != nil { t.Fatal(e) @@ -832,10 +868,11 @@ func TestNodeProxyConnect(t *testing.T) { t.Fatal("proxy route not found") } route = node.ProxyRoute{ + Name: "nodeCproxy@localhost", Proxy: "nodeBproxy@localhost", Cookie: "123", } - nodeA.AddProxyRoute("nodeCproxy@localhost", route) + nodeA.AddProxyRoute(route) e = nodeA.Connect("nodeCproxy@localhost") if e != nil { @@ -846,18 +883,21 @@ func TestNodeProxyConnect(t *testing.T) { fmt.Printf("... connect NodeA to NodeD (with enabled encryption) via NodeB: ") optsD := node.Options{} optsD.Proxy.Cookie = "123" + optsD.Proxy.Accept = true optsD.Proxy.Flags = node.DefaultProxyFlags() optsD.Proxy.Flags.EnableEncryption = true nodeD, e := ergo.StartNode("nodeDproxy@localhost", "secret", optsD) if e != nil { t.Fatal(e) } + defer nodeD.Stop() route = node.ProxyRoute{ + Name: "nodeDproxy@localhost", Proxy: "nodeBproxy@localhost", Cookie: "123", } - nodeA.AddProxyRoute("nodeDproxy@localhost", route) + nodeA.AddProxyRoute(route) e = nodeA.Connect("nodeDproxy@localhost") if e != nil { t.Fatal(e) @@ -932,12 +972,6 @@ func TestNodeProxyConnect(t *testing.T) { fmt.Printf("... processA send 1M message to processD (fragmented, compressed, encrypted): ") pA.Send(pD.Self(), randomString) waitForResultWithValue(t, gsD.v, randomString) - - nodeA.Stop() - nodeB.Stop() - nodeC.Stop() - nodeD.Stop() - } func TestNodeIncarnation(t *testing.T) { @@ -948,10 +982,12 @@ func TestNodeIncarnation(t *testing.T) { if e != nil { t.Fatal(e) } + defer nodeA.Stop() route := node.ProxyRoute{ + Name: "nodeCincarnation@localhost", Proxy: "nodeBincarnation@localhost", } - nodeA.AddProxyRoute("nodeCincarnation@localhost", route) + nodeA.AddProxyRoute(route) // add sleep to get Creation different value for the next node optsB := node.Options{} optsB.Proxy.Transit = true @@ -959,13 +995,16 @@ func TestNodeIncarnation(t *testing.T) { if e != nil { t.Fatal(e) } + defer nodeB.Stop() optsC := node.Options{ Creation: 1234, } + optsC.Proxy.Accept = true nodeC, e := ergo.StartNode("nodeCincarnation@localhost", "secret", optsC) if e != nil { t.Fatal(e) } + defer nodeC.Stop() if err := nodeA.Connect("nodeCincarnation@localhost"); err != nil { t.Fatal(err) @@ -1053,12 +1092,12 @@ func TestNodeIncarnation(t *testing.T) { waitForResultWithValue(t, gsC.v, pC.Self()) fmt.Printf("... processA send a message to previous incarnation of processC (via proxy): ") - if e := pA.Send(pidC, "test"); e != node.ErrProcessIncarnation { + if e := pA.Send(pidC, "test"); e != lib.ErrProcessIncarnation { t.Fatal("must be ErrProcessIncarnation here", e) } fmt.Println("OK") fmt.Printf("... processB send short message to previous incarnation of processC: ") - if e := pB.Send(pidC, "test"); e != node.ErrProcessIncarnation { + if e := pB.Send(pidC, "test"); e != lib.ErrProcessIncarnation { t.Fatal(e) } fmt.Println("OK") @@ -1215,12 +1254,12 @@ type benchGS struct { func (b *benchGS) HandleCall(process *gen.ServerProcess, from gen.ServerFrom, message etf.Term) (etf.Term, gen.ServerStatus) { return etf.Atom("ok"), gen.ServerStatusOK } -func (b *benchGS) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (b *benchGS) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.CallWithTimeout(m.to, m.message, 30) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func BenchmarkNodeSequentialNetwork(b *testing.B) { @@ -1473,9 +1512,10 @@ func BenchmarkNodeProxy_NodeA_to_NodeC_via_NodeB_Message_1K(b *testing.B) { defer node2.Stop() defer node3.Stop() route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) bgs := &benchGS{} @@ -1538,9 +1578,10 @@ func BenchmarkNodeProxy_NodeA_to_NodeC_via_NodeB_Message_1K_Encrypted(b *testing defer node2.Stop() defer node3.Stop() route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) bgs := &benchGS{} @@ -1603,9 +1644,10 @@ func BenchmarkNodeProxy_NodeA_to_NodeC_via_NodeB_Message_1M_Compressed(b *testin defer node2.Stop() defer node3.Stop() route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) bgs := &benchGS{} @@ -1668,9 +1710,10 @@ func BenchmarkNodeProxy_NodeA_to_NodeC_via_NodeB_Message_1M_CompressedEncrypted( defer node2.Stop() defer node3.Stop() route := node.ProxyRoute{ + Name: node3.Name(), Proxy: node2.Name(), } - node1.AddProxyRoute(node3.Name(), route) + node1.AddProxyRoute(route) bgs := &benchGS{} diff --git a/tests/saga_cancel_test.go b/tests/saga_cancel_test.go index 7119edd9..638ae9cc 100644 --- a/tests/saga_cancel_test.go +++ b/tests/saga_cancel_test.go @@ -100,7 +100,7 @@ func (gs *testSagaCancel1) HandleTxResult(process *gen.SagaProcess, id gen.SagaT return gen.SagaStatusOK } -func (gs *testSagaCancel1) HandleSagaDirect(process *gen.SagaProcess, message interface{}) (interface{}, error) { +func (gs *testSagaCancel1) HandleSagaDirect(process *gen.SagaProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { process.StartTransaction(gen.SagaTransactionOptions{}, message) return nil, nil @@ -252,7 +252,7 @@ func (gs *testSagaCancel2) HandleSagaInfo(process *gen.SagaProcess, message etf. return gen.ServerStatusOK } -func (gs *testSagaCancel2) HandleSagaDirect(process *gen.SagaProcess, message interface{}) (interface{}, error) { +func (gs *testSagaCancel2) HandleSagaDirect(process *gen.SagaProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case testSagaStartTX: diff --git a/tests/saga_commit_test.go b/tests/saga_commit_test.go index 7850f917..c07aa008 100644 --- a/tests/saga_commit_test.go +++ b/tests/saga_commit_test.go @@ -104,7 +104,7 @@ type testSagaCommitSendRes struct { id gen.SagaTransactionID } -func (gs *testSagaCommit1) HandleSagaDirect(process *gen.SagaProcess, message interface{}) (interface{}, error) { +func (gs *testSagaCommit1) HandleSagaDirect(process *gen.SagaProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case testSagaCommitStartTx: diff --git a/tests/saga_dist_test.go b/tests/saga_dist_test.go index f6c6e802..cf6ae902 100644 --- a/tests/saga_dist_test.go +++ b/tests/saga_dist_test.go @@ -120,7 +120,7 @@ func (gs *testSaga1) HandleTxDone(process *gen.SagaProcess, id gen.SagaTransacti return nil, gen.SagaStatusOK } -func (gs *testSaga1) HandleSagaDirect(process *gen.SagaProcess, message interface{}) (interface{}, error) { +func (gs *testSaga1) HandleSagaDirect(process *gen.SagaProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case task: values := splitSlice(m.value, m.split) diff --git a/tests/saga_test.go b/tests/saga_test.go index be4a4589..1876fb21 100644 --- a/tests/saga_test.go +++ b/tests/saga_test.go @@ -125,7 +125,7 @@ type taskTX struct { chunks int } -func (gs *testSaga) HandleSagaDirect(process *gen.SagaProcess, message interface{}) (interface{}, error) { +func (gs *testSaga) HandleSagaDirect(process *gen.SagaProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case task: values := splitSlice(m.value, m.split) @@ -138,7 +138,7 @@ func (gs *testSaga) HandleSagaDirect(process *gen.SagaProcess, message interface process.StartTransaction(gen.SagaTransactionOptions{}, txValue) } - return nil, nil + return nil, gen.DirectStatusOK } return nil, fmt.Errorf("unknown request %#v", message) diff --git a/tests/server_test.go b/tests/server_test.go index faa97379..b6099c56 100644 --- a/tests/server_test.go +++ b/tests/server_test.go @@ -3,12 +3,14 @@ package tests import ( "fmt" "reflect" + "sync" "testing" "time" "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -33,7 +35,7 @@ func (tgs *testServer) HandleInfo(process *gen.ServerProcess, message etf.Term) return gen.ServerStatusOK } -func (tgs *testServer) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (tgs *testServer) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) @@ -41,7 +43,7 @@ func (tgs *testServer) HandleDirect(process *gen.ServerProcess, message interfac return nil, process.Cast(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func (tgs *testServer) Terminate(process *gen.ServerProcess, reason string) { tgs.res <- reason @@ -56,8 +58,23 @@ func (tgsd *testServerDirect) Init(process *gen.ServerProcess, args ...etf.Term) tgsd.err <- nil return nil } -func (tgsd *testServerDirect) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { - return message, nil +func (tgsd *testServerDirect) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { + switch m := message.(type) { + case asyncDirect: + m.ref = ref + process.Cast(process.Self(), m) + return nil, gen.DirectStatusIgnore + case syncDirect: + return m.val, gen.DirectStatusOK + } + return message, gen.DirectStatusOK +} +func (tgsd *testServerDirect) HandleCast(process *gen.ServerProcess, message etf.Term) gen.ServerStatus { + switch m := message.(type) { + case asyncDirect: + process.Reply(m.ref, m.val, nil) + } + return gen.ServerStatusOK } func TestServer(t *testing.T) { @@ -281,7 +298,7 @@ func TestServer(t *testing.T) { } fmt.Printf(" process.Direct (without HandleDirect implementation): ") - if _, err := node1gs1.Direct(nil); err == nil { + if _, err := node1gs1.Direct(nil); err != lib.ErrUnsupportedRequest { t.Fatal("must be ErrUnsupportedRequest") } else { fmt.Println("OK") @@ -297,6 +314,19 @@ func TestServer(t *testing.T) { t.Fatal(e) } } + fmt.Printf(" process.Direct (with HandleDirect implementation with async reply): ") + + av := etf.Atom("async direct") + if v1, err := node2gsDirect.Direct(asyncDirect{val: av}); err != nil { + t.Fatal(err) + } else { + if av == v1 { + fmt.Println("OK") + } else { + e := fmt.Errorf("expected: %#v , got: %#v", av, v1) + t.Fatal(e) + } + } fmt.Printf(" process.SetTrapExit(true) and call process.Exit() gs2: ") node1gs2.SetTrapExit(true) @@ -323,6 +353,53 @@ func TestServer(t *testing.T) { node1.Stop() node2.Stop() } +func TestServerDirect(t *testing.T) { + fmt.Printf("\n=== Test Server Direct\n") + fmt.Printf("Starting node: nodeGS1Direct@localhost: ") + node1, _ := ergo.StartNode("nodeGS1Direct@localhost", "cookies", node.Options{}) + if node1 == nil { + t.Fatal("can't start nodes") + } else { + fmt.Println("OK") + } + defer node1.Stop() + + gsDirect := &testServerDirect{ + err: make(chan error, 2), + } + + fmt.Printf(" wait for start of gsDirect on %#v: ", node1.Name()) + node1gsDirect, _ := node1.Spawn("gsDirect", gen.ProcessOptions{}, gsDirect, nil) + waitForResult(t, gsDirect.err) + + var wg sync.WaitGroup + + fmt.Println(" process.Direct with 1000 goroutines:") + direct := func() { + v := etf.Atom("sync direct") + defer wg.Done() + repeat: + if v1, err := node1gsDirect.Direct(syncDirect{val: v}); err != nil { + if err == lib.ErrProcessBusy { + goto repeat + } + t.Fatal(err) + } else { + if v != v1 { + e := fmt.Errorf("expected: %#v , got: %#v", v, v1) + t.Fatal(e) + } + } + } + n := 1000 + for i := 0; i < n; i++ { + wg.Add(1) + go direct() + } + + wg.Wait() + fmt.Println("OK") +} type messageOrderGS struct { gen.Server @@ -391,7 +468,7 @@ func (gs *messageOrderGS) HandleCall(process *gen.ServerProcess, from gen.Server return nil, fmt.Errorf("incorrect call") } -func (gs *messageOrderGS) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (gs *messageOrderGS) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case testCase3: for i := 0; i < m.n; i++ { @@ -403,7 +480,7 @@ func (gs *messageOrderGS) HandleDirect(process *gen.ServerProcess, message inter panic("wrong result") } } - return nil, nil + return nil, gen.DirectStatusOK } return nil, fmt.Errorf("incorrect direct call") @@ -426,7 +503,7 @@ func (gs *GSCallPanic) HandleCall(process *gen.ServerProcess, from gen.ServerFro return "ok", gen.ServerStatusOK } -func (gs *GSCallPanic) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (gs *GSCallPanic) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { pids, ok := message.([]etf.Pid) if !ok { @@ -448,7 +525,7 @@ func (gs *GSCallPanic) HandleDirect(process *gen.ServerProcess, message interfac } fmt.Println("OK") - return nil, nil + return nil, gen.DirectStatusOK } func TestServerCallServerWithPanic(t *testing.T) { diff --git a/tests/stage_test.go b/tests/stage_test.go index afde1e57..14baa028 100644 --- a/tests/stage_test.go +++ b/tests/stage_test.go @@ -8,6 +8,7 @@ import ( "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -126,7 +127,7 @@ func (gs *StageProducerTest) SetAutoDemand(p gen.Process, subscription gen.Stage return nil } -func (s *StageProducerTest) HandleStageDirect(process *gen.StageProcess, message interface{}) (interface{}, error) { +func (s *StageProducerTest) HandleStageDirect(process *gen.StageProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case demandHandle: process.SetDemandHandle(m.enable) @@ -146,7 +147,7 @@ func (s *StageProducerTest) HandleStageDirect(process *gen.StageProcess, message return nil, process.Cast(m.to, m.message) default: - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } } @@ -186,7 +187,7 @@ func (gs *StageConsumerTest) HandleStageInfo(process *gen.StageProcess, message return gen.ServerStatusOK } -func (s *StageConsumerTest) HandleStageDirect(p *gen.StageProcess, message interface{}) (interface{}, error) { +func (s *StageConsumerTest) HandleStageDirect(p *gen.StageProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case newSubscription: return p.Subscribe(m.producer, m.opts) @@ -205,7 +206,7 @@ func (s *StageConsumerTest) HandleStageDirect(p *gen.StageProcess, message inter case makeCast: return nil, p.Cast(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func (s *StageConsumerTest) HandleStageTerminate(p *gen.StageProcess, reason string) { diff --git a/tests/supervisor_ofo_test.go b/tests/supervisor_ofo_test.go index 32dace72..56b1ce11 100644 --- a/tests/supervisor_ofo_test.go +++ b/tests/supervisor_ofo_test.go @@ -31,6 +31,7 @@ import ( "github.com/ergo-services/ergo" "github.com/ergo-services/ergo/etf" "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/lib" "github.com/ergo-services/ergo/node" ) @@ -84,14 +85,14 @@ func (tsv *testSupervisorGenServer) HandleCall(process *gen.ServerProcess, from return message, gen.ServerStatusOK } -func (tsv *testSupervisorGenServer) HandleDirect(process *gen.ServerProcess, message interface{}) (interface{}, error) { +func (tsv *testSupervisorGenServer) HandleDirect(process *gen.ServerProcess, ref etf.Ref, message interface{}) (interface{}, gen.DirectStatus) { switch m := message.(type) { case makeCall: return process.Call(m.to, m.message) case makeCast: return nil, process.Cast(m.to, m.message) } - return nil, gen.ErrUnsupportedRequest + return nil, lib.ErrUnsupportedRequest } func (tsv *testSupervisorGenServer) Terminate(process *gen.ServerProcess, reason string) { diff --git a/tests/tcp_test.go b/tests/tcp_test.go new file mode 100644 index 00000000..13b09e56 --- /dev/null +++ b/tests/tcp_test.go @@ -0,0 +1,137 @@ +package tests + +import ( + "fmt" + "net" + "testing" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +var ( + resChan = make(chan interface{}, 2) +) + +type testTCPHandler struct { + gen.TCPHandler +} + +type messageTestTCPConnect struct{} +type messageTestTCPStatusNext struct { + left int + await int +} + +func (r *testTCPHandler) HandleConnect(process *gen.TCPHandlerProcess, conn *gen.TCPConnection) gen.TCPHandlerStatus { + resChan <- messageTestTCPConnect{} + return gen.TCPHandlerStatusOK +} + +func (r *testTCPHandler) HandlePacket(process *gen.TCPHandlerProcess, packet []byte, conn *gen.TCPConnection) (int, int, gen.TCPHandlerStatus) { + l := len(packet) + //fmt.Println("GOT", process.Self(), packet, l, "bytes", l%10) + if l < 10 { + resChan <- messageTestTCPStatusNext{ + left: l, + await: 10 - l, + } + return l, 10 - l, gen.TCPHandlerStatusOK + } + if l > 10 { + resChan <- messageTestTCPStatusNext{ + left: l % 10, + await: 10 - (l % 10), + } + return l % 10, 10 - (l % 10), gen.TCPHandlerStatusOK + } + resChan <- packet + return 0, 0, gen.TCPHandlerStatusOK +} + +type testTCPServer struct { + gen.TCP +} + +func (ts *testTCPServer) InitTCP(process *gen.TCPProcess, args ...etf.Term) (gen.TCPOptions, error) { + var options gen.TCPOptions + options.Handler = &testTCPHandler{} + options.Port = 10101 + + return options, nil +} + +func TestTCP(t *testing.T) { + fmt.Printf("\n=== Test TCP Server\n") + fmt.Printf("Starting nodes: nodeTCP1@localhost: ") + node1, err := ergo.StartNode("nodeTCP1@localhost", "cookies", node.Options{}) + defer node1.Stop() + if err != nil { + t.Fatal("can't start node", err) + } else { + fmt.Println("OK") + } + + fmt.Printf("...starting process (gen.TCP): ") + tcpProcess, err := node1.Spawn("tcp", gen.ProcessOptions{}, &testTCPServer{}) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("...makeing a new connection: ") + conn, err := net.Dial("tcp", "localhost:10101") + if err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, resChan, messageTestTCPConnect{}) + + fmt.Printf("...send/recv data (10 bytes as 1 logic dataframe): ") + testData1 := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + if _, err := conn.Write(testData1); err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, resChan, testData1) + + fmt.Printf("...send/recv data (7 bytes as a part of logic dataframe): ") + testData2 := []byte{11, 12, 13, 14, 15, 16, 17} + if _, err := conn.Write(testData2); err != nil { + t.Fatal(err) + } + + value := messageTestTCPStatusNext{ + left: 7, + await: 3, + } + + waitForResultWithValue(t, resChan, value) + + fmt.Printf("...send/recv data (5 bytes, must be 1 logic dataframe + extra 2 bytes): ") + testData2 = []byte{18, 19, 20, 21, 22} + if _, err := conn.Write(testData2); err != nil { + t.Fatal(err) + } + value = messageTestTCPStatusNext{ + left: 2, + await: 8, + } + waitForResultWithValue(t, resChan, value) + + fmt.Printf("...send/recv data (8 bytes, must be 1 logic dataframe): ") + testData2 = []byte{23, 24, 25, 26, 27, 28, 29, 30} + if _, err := conn.Write(testData2); err != nil { + t.Fatal(err) + } + waitForResultWithValue(t, resChan, []byte{21, 22, 23, 24, 25, 26, 27, 28, 29, 30}) + + tcpProcess.Kill() + tcpProcess.Wait() + + fmt.Printf("...stopping process (gen.TCP): ") + if _, err := net.Dial("tcp", "localhost:10101"); err == nil { + t.Fatal("error must be here") + } + fmt.Println("OK") +} diff --git a/tests/udp_test.go b/tests/udp_test.go new file mode 100644 index 00000000..e974ae07 --- /dev/null +++ b/tests/udp_test.go @@ -0,0 +1,74 @@ +package tests + +import ( + "fmt" + "net" + "testing" + "time" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +var ( + resUDPChan = make(chan interface{}, 2) +) + +type testUDPHandler struct { + gen.UDPHandler +} + +func (r *testUDPHandler) HandlePacket(process *gen.UDPHandlerProcess, data []byte, packet gen.UDPPacket) { + resUDPChan <- data + return +} + +type testUDPServer struct { + gen.UDP +} + +func (ts *testUDPServer) InitUDP(process *gen.UDPProcess, args ...etf.Term) (gen.UDPOptions, error) { + var options gen.UDPOptions + options.Handler = &testUDPHandler{} + options.Port = 10101 + + return options, nil +} + +func TestUDP(t *testing.T) { + fmt.Printf("\n=== Test UDP Server\n") + fmt.Printf("Starting nodes: nodeUDP1@localhost: ") + node1, err := ergo.StartNode("nodeUDP1@localhost", "cookies", node.Options{}) + defer node1.Stop() + if err != nil { + t.Fatal("can't start node", err) + } else { + fmt.Println("OK") + } + + fmt.Printf("...starting process (gen.UDP): ") + udpProcess, err := node1.Spawn("udp", gen.ProcessOptions{}, &testUDPServer{}) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("...send/receive data: ") + c, err := net.Dial("udp", "localhost:10101") + if err != nil { + t.Fatal(err) + } + defer c.Close() + data := []byte{1, 2, 3, 4, 5} + c.Write(data) + waitForResultWithValue(t, resUDPChan, data) + + fmt.Printf("...stopping process (gen.UDP): ") + udpProcess.Kill() + if err := udpProcess.WaitWithTimeout(time.Second); err != nil { + t.Fatal(err) + } + fmt.Println("OK") +} diff --git a/tests/web_test.go b/tests/web_test.go new file mode 100644 index 00000000..ce014996 --- /dev/null +++ b/tests/web_test.go @@ -0,0 +1,74 @@ +package tests + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/ergo-services/ergo" + "github.com/ergo-services/ergo/etf" + "github.com/ergo-services/ergo/gen" + "github.com/ergo-services/ergo/node" +) + +var ( + testWebString = "hello world" +) + +type testWebHandler struct { + gen.WebHandler +} + +func (r *testWebHandler) HandleRequest(process *gen.WebHandlerProcess, request gen.WebMessageRequest) gen.WebHandlerStatus { + request.Response.Write([]byte(testWebString)) + return gen.WebHandlerStatusDone +} + +type testWebServer struct { + gen.Web +} + +func (w *testWebServer) InitWeb(process *gen.WebProcess, args ...etf.Term) (gen.WebOptions, error) { + var options gen.WebOptions + + mux := http.NewServeMux() + webHandler := process.StartWebHandler(&testWebHandler{}, gen.WebHandlerOptions{}) + mux.Handle("/", webHandler) + options.Handler = mux + + return options, nil +} + +func TestWeb(t *testing.T) { + fmt.Printf("\n=== Test Web Server\n") + fmt.Printf("Starting nodes: nodeWeb1@localhost: ") + node1, err := ergo.StartNode("nodeWeb1@localhost", "cookies", node.Options{}) + defer node1.Stop() + if err != nil { + t.Fatal("can't start node", err) + } else { + fmt.Println("OK") + } + + fmt.Printf("...starting process (gen.Web): ") + _, err = node1.Spawn("web", gen.ProcessOptions{}, &testWebServer{}) + if err != nil { + t.Fatal(err) + } + fmt.Println("OK") + + fmt.Printf("...making simple GET request: ") + res, err := http.Get("http://localhost:8080") + if err != nil { + t.Fatal(err) + } + out, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if string(out) != testWebString { + t.Fatal("mismatch result") + } + fmt.Println("OK") +} diff --git a/version.go b/version.go index 507ec0e5..03145a08 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,7 @@ package ergo const ( - Version = "2.1.1" // Ergo Framework version + Version = "2.2.0" // Ergo Framework version VersionPrefix = "ergo" // Prefix using for the full version name VersionOTP int = 24 // Erlang version support )