diff --git a/README.md b/README.md index 569aea1..3643eeb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # leaky-pool -A connection pool that leaks on purpose \ No newline at end of file +A working connection pool that leaks on purpose for demonstration purposes. \ No newline at end of file diff --git a/client/README.MD b/client/README.MD new file mode 100644 index 0000000..e088868 --- /dev/null +++ b/client/README.MD @@ -0,0 +1,2 @@ +# leaky-pool client + diff --git a/client/client.exe b/client/client.exe new file mode 100644 index 0000000..deee09b Binary files /dev/null and b/client/client.exe differ diff --git a/client/go.mod b/client/go.mod new file mode 100644 index 0000000..9ea89bb --- /dev/null +++ b/client/go.mod @@ -0,0 +1,12 @@ +module deadbeef.codes/steven/leaky-pool/client + +go 1.20 + +require github.com/g3n/engine v0.2.0 + +require ( + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/client/go.sum b/client/go.sum new file mode 100644 index 0000000..f3845bf --- /dev/null +++ b/client/go.sum @@ -0,0 +1,14 @@ +github.com/g3n/engine v0.2.0 h1:7dmj4c+3xHcBnYrVmRuVf/oZ2JycxJU9Y+2FQj1Af2Y= +github.com/g3n/engine v0.2.0/go.mod h1:rnj8jiLdKEDI8VbveKhmdL4rovjjy+uxNP5YROg2x8g= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9 h1:D0iM1dTCbD5Dg1CbuvLC/v/agLc79efSj/L35Q3Vqhs= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..21fdc30 --- /dev/null +++ b/client/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "log" + "strconv" + "time" + + "github.com/g3n/engine/app" + "github.com/g3n/engine/camera" + "github.com/g3n/engine/core" + "github.com/g3n/engine/geometry" + "github.com/g3n/engine/gls" + "github.com/g3n/engine/graphic" + "github.com/g3n/engine/gui" + "github.com/g3n/engine/light" + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" + "github.com/g3n/engine/renderer" + "github.com/g3n/engine/util/helper" + "github.com/g3n/engine/window" +) + +func main() { + + // Create application and scene + a := app.App(1920, 1080, "Leaky Pool") + + scene := core.NewNode() + + // Set the scene to be managed by the gui manager + gui.Manager().Set(scene) + + // Create perspective camera + cam := camera.New(1) + cam.SetPosition(0, 0, 80) + scene.Add(cam) + + // Set up orbit control for the camera + camera.NewOrbitControl(cam) + + // Set up callback to update viewport and camera aspect ratio when the window is resized + onResize := func(evname string, ev interface{}) { + // Get framebuffer size and update viewport accordingly + width, height := a.GetSize() + a.Gls().Viewport(0, 0, int32(width), int32(height)) + // Update the camera's aspect ratio + cam.SetAspect(float32(width) / float32(height)) + } + a.Subscribe(window.OnWindowSize, onResize) + onResize("", nil) + + // Create a blue torus and add it to the scene + geom := geometry.NewTorus(1, .4, 12, 32, math32.Pi*2) + + mat := material.NewStandard(math32.NewColor("DarkBlue")) + mesh := graphic.NewMesh(geom, mat) + + scene.Add(mesh) + + // Create and add lights to the scene + scene.Add(light.NewAmbient(&math32.Color{1.0, 1.0, 1.0}, 0.8)) + pointLight := light.NewPoint(&math32.Color{1, 1, 1}, 5.0) + pointLight.SetPosition(1, 0, 2) + scene.Add(pointLight) + + // Create and add an axis helper to the scene + scene.Add(helper.NewAxes(0.5)) + + // Set background color to gray + //a.Gls().ClearColor(0.5, 0.5, 0.5, 1.0) + a.Gls().ClearColor(0, 0, 0, 1.0) + + /////// + //GUI// + /////// + + labelServer := gui.NewLabel("Server: ") + labelServer.SetPosition(10, 8) + scene.Add(labelServer) + + editServer := gui.NewEdit(150, "10.69.71.106:6699") + editServer.SetText("10.69.71.106:6699") + editServer.SetPosition(labelServer.Position().X+labelServer.Width()+10, 10) + scene.Add(editServer) + + labelPoolSize := gui.NewLabel("Pool Size: ") + labelPoolSize.SetPosition(editServer.Position().X+editServer.Width()+10, 8) + scene.Add(labelPoolSize) + + editPoolSize := gui.NewEdit(30, "10") + editPoolSize.SetText("10") + editPoolSize.SetPosition(labelPoolSize.Position().X+labelPoolSize.Width()+10, 10) + scene.Add(editPoolSize) + + buttonConnect := gui.NewButton("Start Pool") + buttonConnect.SetPosition(editPoolSize.Position().X+editPoolSize.Width()+10, 8) + buttonConnect.Subscribe(gui.OnClick, func(name string, ev interface{}) { + + poolSize, err := strconv.Atoi(editPoolSize.Text()) + if err != nil { + log.Printf("failed to convert poolSize '%d' to integer: %v", poolSize, err) + return + } + go connPoolWatchdog(editServer.Text(), poolSize, scene) // goroutine that keeps the pool full of healthy TCP connections + + buttonConnect.Label.SetText("Stop Pool") // stop not implemented + + }) + scene.Add(buttonConnect) + + // Create and add a button to the scene + btn := gui.NewButton("Make Red") + btn.SetPosition(100, 40) + btn.SetSize(40, 40) + btn.Subscribe(gui.OnClick, func(name string, ev interface{}) { + mat.SetColor(math32.NewColor("DarkRed")) + }) + scene.Add(btn) + + // Run the application + a.Run(func(renderer *renderer.Renderer, deltaTime time.Duration) { + a.Gls().Clear(gls.DEPTH_BUFFER_BIT | gls.STENCIL_BUFFER_BIT | gls.COLOR_BUFFER_BIT) + + renderer.Render(scene, cam) + }) +} diff --git a/client/pool.go b/client/pool.go new file mode 100644 index 0000000..5437f35 --- /dev/null +++ b/client/pool.go @@ -0,0 +1,104 @@ +package main + +import ( + "log" + "net" + "time" + + "github.com/g3n/engine/core" + "github.com/g3n/engine/geometry" + "github.com/g3n/engine/graphic" + "github.com/g3n/engine/material" + "github.com/g3n/engine/math32" +) + +var dials []Dial + +type Dial struct { + Connection net.Conn // the real deal underlying TCP connection + Geo *geometry.Geometry // the 3D geometry + Mat *material.Standard // the material / color + Mesh *graphic.Mesh // the combined geometry and material + Node *core.Node // pointer to node in scene, required for removing the object if connection goes down + +} + +// Keeps the pool full, replaces stale connections, and at the same time creates the objects in the 3D scenes representing physical connections +// should be ran in its own goroutine +func connPoolWatchdog(serverAddress string, maxPoolSize int, scene *core.Node) { + + dials = make([]Dial, 0) + + tcpAddr, err := net.ResolveTCPAddr("tcp4", serverAddress) + if err != nil { + log.Fatalf("failed to resolve serverAddress '%s': %v", serverAddress, err) + } + + for { + + removed := 0 + // Check status of existing open connections in pool + for i, dial := range dials { + if !isConnUp(dial.Connection) { // TBD: this should be moved to check before first write to the socket file descriptor instead of polling here + log.Printf("closing bad idle connection and removing from pool - %s", dial.Connection.LocalAddr().String()) + dial.Connection.Close() + removeCompleted := scene.Remove(dial.Mesh) + log.Printf("removed: %v", removeCompleted) + + dials = append(dials[:i-removed], dials[i-removed+1:]...) + removed++ + } + } + + // fill any empty slots in the pool with fresh connections + for poolSize := len(dials); poolSize < maxPoolSize; poolSize++ { + log.Printf("Current pool size is '%d', desired pool size is '%d' - opening new connection...", poolSize, maxPoolSize) + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + log.Printf("failed to dial TCP connection: %v", err) + break + } + + conn.SetKeepAlive(true) + conn.SetKeepAlivePeriod(time.Second * 5) + + dial := Dial{Connection: conn, Geo: geometry.NewBox(1, 1, 1), Mat: material.NewStandard(math32.NewColor("LimeGreen"))} + dial.Mesh = graphic.NewMesh(dial.Geo, dial.Mat) + dial.Node = scene.Add(dial.Mesh) + dials = append(dials, dial) + } + + // Update the position of the connections on the scene + padding := float32(200 / (len(dials) + 1)) // space between the connection objects in 3D space + currX := float32(len(dials)/2) * -padding // variable gets updated, this is the initial starting position (left most connection in 3D space) + if len(dials)%2 == 0 { + currX += padding / 2 + } + //currX := padding - 100 + for _, dial := range dials { + dial.Mesh.SetPositionX(currX) + currX += padding + } + + time.Sleep(time.Second * 5) // random sleep, AKA evidence this shouldn't be a watchdog, + // this whole function should be event based instead of polling + } +} + +// Checks with OS to ensure that a connection is still active +// returns err if connection is not active +func isConnUp(conn net.Conn) bool { + conn.SetReadDeadline(time.Now().Add(time.Millisecond * 300)) + + buf := make([]byte, 128) + _, err := conn.Read(buf) + if err != nil { + log.Printf("connection error detected: %v", err) + return false + } + + var zero time.Time + conn.SetReadDeadline(zero) + + return true +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..184b963 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.20 + +use ( + ./client + ./server +) diff --git a/server/README.MD b/server/README.MD new file mode 100644 index 0000000..c537659 --- /dev/null +++ b/server/README.MD @@ -0,0 +1,4 @@ +# leaky-pool server + +Listens on port 8080 for new connections, accepts them and then closes them after one minute. + diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..8110e5b --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module deadbeef.codes/steven/leaky-pool/server + +go 1.20 diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..e57838b --- /dev/null +++ b/server/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "log" + "net" + "time" +) + +func main() { + listenerAddr, err := net.ResolveTCPAddr("tcp4", ":6699") + if err != nil { + log.Fatalf("could not resolve listenAddr: %v", err) + } + + listener, err := net.ListenTCP("tcp", listenerAddr) + if err != nil { + log.Fatalf("could not listen on address '%s': %v", listenerAddr.String(), err) + } + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("failed to accept new incoming connection: %v", err) // if logging facility has latency this will block + continue + } + go connHandler(conn) // branch off into goroutine so we're not blocking + } +} + +func connHandler(conn net.Conn) { + log.Printf("accepted new connection from: %s", conn.RemoteAddr().String()) + + conn.SetReadDeadline(time.Now().Add(time.Second)) + + for { + + buf := make([]byte, 128) + bytesRead, err := conn.Read(buf) + if err == nil && bytesRead > 0 { + log.Printf("Incoming message over socket '%s': %s", conn.RemoteAddr().String(), string(buf)) + } + + _, err = conn.Write([]byte("1")) + if err != nil { + log.Printf("failed writing to connection '%s': %v", conn.RemoteAddr().String(), err) + conn.Close() + break + } + + time.Sleep(time.Second) + } + +}