pkservices¶
package pkservices offers a lightweight framework for managing the lifetime of multiple services and their resource dependencies (db connector, etc).
Quickstart¶
First we’ll start by implementing a simple gRPC server. The service we are implementing is pkservices.PingServer.
We also need to implement the pkservices.GrpcService interface for our server manager to know what to do with our service.
// pingService is a basic implementation of PingServer that the manager can use to test
// connectivity to the server.
type PingService struct {
}
// Id implements Service and returns "gPEAKERC Ping".
func (ping pingService) Id() string {
return "gPEAKERC Ping"
}
// Setup implements Service.
func (ping pingService) Setup(
resourcesCtx context.Context,
resourcesReleased *sync.WaitGroup,
shutdownCtx context.Context
logger zerolog.Logger,
) error {
return nil
}
// RegisterOnServer implements GrpcService.
func (ping pingService) RegisterOnServer(server *grpc.Server) {
protogen.RegisterPingServer(server, ping)
}
// Ping implements PingServer. It receives an empty message and returns the
// result.
func (ping pingService) Ping(
ctx context.Context, msg *empty.Empty,
) (*empty.Empty, error) {
return msg, nil
}
The Setup()
method allows us to spin up any resources our service needs to run. The
resourcesCtx
passed to the method will not be released until after our gRPC server
has gracefully shutdown, and the manager we are about to register our service with will
not fully exit until resourcesReleased
is fully closed.
Let’s run out service in a Manager
:
func main() {
// Our manager options.
managerOpts := pkservices.NewManagerOpts().
// We are adding our own implementation of Ping, so we don't need to register
// The default one.
WithGrpcPingService(false)
// Create a new manager to manage our service.
manager := pkservices.NewManager(managerOpts, ExampleService{})
// Run the manager in it's own goroutine and return errors to our error channel.
errChan := make(chan error)
go func() {
defer close(errChan)
errChan <- manager.Run()
}()
// make a gRPC client to ping the service
clientConn, err := grpc.Dial(pkservices.DefaultGrpcAddress, grpc.WithInsecure())
if err != nil {
panic(err)
}
// create a new client to interact with our server
pingClient := pkservices.NewPingClient(clientConn)
// Wait for the server to be serving requests.
err = pkclients.WaitForGrpcServer(context.Background(), clientConn)
if err != nil {
panic(err)
}
// Send a ping
_, err = pingClient.Ping(context.Background(), new(emptypb.Empty))
if err != nil {
panic(err)
}
// Start shutdown.
manager.StartShutdown()
// Grab our error from the error channel (blocks until the manager is shutdown)
err = <- errChan
if err != nil {
panic(err)
}
// Exit.
// Output:
// PING RECEIVED!
}
This top-level logic is all you need to run your services!
Service Interfaces¶
pkservices defines three interfaces for declaring services: Service
, GrpcService
and GenericService
.
These interfaces are designed for the quick declaration of services, which can then
be handed off to the pkservices.Manager
type for running. The end result is
having to write very little code for the boilerplate of managing the lifetime of the
service.
Note
Service
acts as a base for more specific service types. A service must implement
one of the more specific types (like ServiceGrpc
), and not just Service
for
the manager to run it.
The base Service
interface looks like this:
type Service interface {
// Id should return a unique, but human readable id for the service. This id is
// used for both logging and ServiceError context. If two services share the same
// id, the manger will return an error.
Id() string
// Setup is called before Run to set up any resources the service requires.
//
// resourcesCtx will be cancelled AFTER Run returns to signal that all the main
// process has finished, and it is safe to release resources.
//
// resourcesReleased should be incremented and decremented by individual resources,
// and the Manager will block on it until the context passed to Shutdown cancels.
//
// logger is a zerolog.Logger with a
// zerolog.Logger.WithString("SERVICE", [Service.Id()]) entry already on it.
Setup(
resourcesCtx context.Context,
resourcesReleased *sync.WaitGroup,
logger zerolog.Logger,
) error
}
Testing Methods¶
The Manager
type exposes a number of useful tasting methods, which can be accessed
through Manager.Test()
. See the API docs for more details.