The main use case - is a Docker health check (liveness/readiness probes) for CLI only applications (without exposed tcp/udp ports).
Last active
January 6, 2023 10:25
-
-
Save tarampampam/cd8ac1dca118917c365b3042ec95b3ef to your computer and use it in GitHub Desktop.
Golang simple RPC client/server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc | |
type ( | |
API struct{} | |
LivenessArgs struct{} | |
LivenessReply struct{ Ok bool } | |
) | |
// NewAPI returns a new API instance. | |
func NewAPI() *API { | |
return &API{} | |
} | |
// Liveness is a handler to check if the server is alive. | |
func (*API) Liveness(_ *LivenessArgs, reply *LivenessReply) error { | |
reply.Ok = true | |
return nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc_test | |
import ( | |
"testing" | |
"github.com/stretchr/testify/assert" | |
"app/internal/rpc" | |
) | |
func TestAPI_Liveness(t *testing.T) { | |
var in, out = rpc.LivenessArgs{}, rpc.LivenessReply{} | |
assert.NoError(t, rpc.NewAPI().Liveness(&in, &out)) | |
assert.True(t, out.Ok) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc | |
import ( | |
"errors" | |
"net/rpc" | |
) | |
// SocketClient is a client for a SocketServer. | |
// Dial and Close are NOT safe for concurrent use. | |
type SocketClient struct { | |
socketPath string | |
client *rpc.Client | |
isStarted bool | |
} | |
// NewSocketClient returns a new SocketClient. | |
func NewSocketClient(socketPath string) *SocketClient { | |
return &SocketClient{socketPath: socketPath} | |
} | |
// Dial establishes a connection to the SocketServer. | |
func (c *SocketClient) Dial() (err error) { | |
if c.isStarted { | |
return errors.New("already started") | |
} | |
if c.client, err = rpc.Dial("unix", c.socketPath); err != nil { | |
return err | |
} | |
c.isStarted = true | |
return | |
} | |
// CallLiveness checks if the server is alive. | |
func (c *SocketClient) CallLiveness() (LivenessReply, error) { | |
var args, reply = LivenessArgs{}, LivenessReply{} | |
if err := c.client.Call("API.Liveness", &args, &reply); err != nil { | |
return LivenessReply{}, err | |
} | |
return reply, nil | |
} | |
// Close closes the client (including the underlying connection). | |
func (c *SocketClient) Close() error { | |
if !c.isStarted { | |
return errors.New("not started") | |
} | |
defer func() { c.isStarted = false }() | |
if err := c.client.Close(); err != nil { | |
return err | |
} | |
return nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc_test | |
import ( | |
"os" | |
"testing" | |
"github.com/stretchr/testify/assert" | |
"go.uber.org/goleak" | |
"app/internal/rpc" | |
) | |
func TestSocketClient_DialAndClose(t *testing.T) { | |
defer goleak.VerifyNone(t) | |
// empty socket path | |
assert.ErrorContains(t, rpc.NewSocketClient("").Dial(), "missing address") | |
var tempFilePath string | |
{ // create empty file | |
tempFile, err := os.CreateTemp("", "") | |
assert.NoError(t, err) | |
defer func() { assert.NoError(t, os.Remove(tempFile.Name())) }() | |
assert.NoError(t, tempFile.Close()) | |
assert.FileExists(t, tempFile.Name()) | |
tempFilePath = tempFile.Name() | |
} | |
// socket path is a regular file | |
assert.ErrorContains(t, rpc.NewSocketClient(tempFilePath).Dial(), "connection refused") | |
// non-existent socket path | |
assert.ErrorContains(t, rpc.NewSocketClient(tempSocketFilePath(t)).Dial(), "no such file") | |
var ( | |
socketPath = tempSocketFilePath(t) | |
srv = rpc.NewSocketServer(socketPath) | |
) | |
assert.NoError(t, srv.Start()) | |
defer func() { assert.NoError(t, srv.Close()) }() | |
// normal usage | |
var client = rpc.NewSocketClient(socketPath) | |
assert.NoError(t, client.Dial()) | |
assert.NoError(t, client.Close()) | |
assert.Error(t, client.Close()) // already closed | |
} | |
func TestSocketClient_CallLiveness(t *testing.T) { | |
defer goleak.VerifyNone(t) | |
var ( | |
socketPath = tempSocketFilePath(t) | |
server = rpc.NewSocketServer(socketPath) | |
client = rpc.NewSocketClient(socketPath) | |
) | |
// prepare server | |
assert.NoError(t, server.Register()) | |
assert.NoError(t, server.Start()) | |
assert.ErrorContains(t, server.Start(), "already started") | |
defer func() { assert.ErrorContains(t, server.Close(), "not started") }() | |
// prepare client | |
assert.NoError(t, client.Dial()) | |
assert.ErrorContains(t, client.Dial(), "already started") | |
defer func() { assert.ErrorContains(t, client.Close(), "not started") }() | |
// call | |
resp, err := client.CallLiveness() | |
assert.NoError(t, err) | |
assert.True(t, resp.Ok) | |
// and close | |
assert.NoError(t, server.Close()) | |
assert.NoError(t, client.Close()) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc | |
import ( | |
"net" | |
"net/rpc" | |
"github.com/pkg/errors" | |
) | |
// SocketServer is a server for a SocketClient. | |
// Start and Close are NOT safe for concurrent use. | |
type SocketServer struct { | |
rpc *rpc.Server | |
socketPath string | |
listener net.Listener | |
isStarted bool | |
} | |
// NewSocketServer returns a new SocketServer. | |
func NewSocketServer(socketPath string) *SocketServer { | |
return &SocketServer{socketPath: socketPath, rpc: rpc.NewServer()} | |
} | |
// Register the handlers. | |
func (s *SocketServer) Register() error { | |
if err := s.rpc.RegisterName("API", NewAPI()); err != nil { | |
return err | |
} | |
return nil | |
} | |
// Start the RPC server. | |
func (s *SocketServer) Start() (err error) { | |
if s.isStarted { | |
return errors.New("already started") | |
} | |
if s.socketPath == "" { | |
return errors.New("missing address") | |
} | |
// create a Unix domain socket and listen for incoming connections | |
if s.listener, err = net.Listen("unix", s.socketPath); err != nil { | |
return errors.Wrap(err, "failed to listen on socket") | |
} | |
s.isStarted = true | |
go s.rpc.Accept(s.listener) | |
return | |
} | |
// Close stops the RPC server. | |
func (s *SocketServer) Close() error { | |
if !s.isStarted { | |
return errors.New("not started") | |
} | |
defer func() { s.isStarted = false }() | |
if err := s.listener.Close(); err != nil { | |
return err | |
} | |
return nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc_test | |
import ( | |
"os" | |
"testing" | |
"github.com/stretchr/testify/assert" | |
"go.uber.org/goleak" | |
"app/internal/rpc" | |
) | |
func TestSocketServer_StartAndStop(t *testing.T) { | |
defer goleak.VerifyNone(t) | |
// empty socket path | |
assert.ErrorContains(t, rpc.NewSocketServer("").Start(), "missing address") | |
var tempFilePath string | |
{ // create empty file | |
tempFile, err := os.CreateTemp("", "") | |
assert.NoError(t, err) | |
defer func() { assert.NoError(t, os.Remove(tempFile.Name())) }() | |
assert.NoError(t, tempFile.Close()) | |
assert.FileExists(t, tempFile.Name()) | |
tempFilePath = tempFile.Name() | |
} | |
// socket path is a regular file | |
assert.ErrorContains(t, rpc.NewSocketServer(tempFilePath).Start(), "address already in use") | |
// normal usage | |
var ( | |
socketPath = tempSocketFilePath(t) | |
srv = rpc.NewSocketServer(socketPath) | |
) | |
assert.NoFileExists(t, socketPath) | |
assert.NoError(t, srv.Start()) // start | |
assert.FileExists(t, socketPath) | |
assert.ErrorContains(t, srv.Start(), "already started") // repeated start | |
assert.ErrorContains(t, srv.Start(), "already started") // repeated start | |
assert.NoError(t, srv.Close()) // stop | |
assert.NoFileExists(t, socketPath) | |
assert.ErrorContains(t, srv.Close(), "not started") // repeated close | |
assert.ErrorContains(t, srv.Close(), "not started") // repeated close | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package rpc_test | |
import ( | |
"os" | |
"path" | |
"strconv" | |
"testing" | |
"time" | |
) | |
func tempSocketFilePath(t *testing.T) string { | |
t.Helper() | |
return path.Join(os.TempDir(), "unit-test-"+strconv.Itoa(int(time.Now().UnixNano()))+".sock") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment