October, 2019

Microservices Infrastructure

This month focus is on microservices infrastracture related to management of multiple processes. It includes:

Hivemind

I use hivemind when I need to run a set of microservices at the same time during development.

Good Planning Results in Fast Microservice Tests

It is not a secret that microservices usually have a dependency graph. When time comes to test microservices it can be challenging to setup a proper testing environment.

If a proper effort has been put in how to create microservice at application level, then it is not difficult to host all microservices inside a single “fat” test process. The advantage of this approach is in short development cycle time.

The following example is a content of a test case for private logger microservce written in GO language. The test case starts 2 microservices in the same test process prior to running any tests and ensures that microservices properly shutdown as well.

package logger_test

import (
	"log"
	"os"
	"testing"

	"github.com/golang/protobuf/ptypes"
	"github.com/nats-io/nats.go"

	"gitlab.com/sergei.gnezdov/idcs/backend/services/config"
	"gitlab.com/sergei.gnezdov/idcs/backend/services/logger"
	"gitlab.com/sergei.gnezdov/idcs/backend/services/servicebase"
)

// loads test configuration from filesystem
// replaces microservice arguments on startup
var appFlags = servicebase.TestEnvService()

// TestMain is an entry point when
// custom setup needs to be done before and after tests.
//
// In this case we start 2 microservices in the same process.
//
// The tests will still run even if this method is commented out.
// If it is commented out, then microservices need to be started
// outside of the test process prior to running the test.
func TestMain(m *testing.M) {
	var err error

	confSrv := config.NewServer(appFlags.BootConfig)
	logSrv := logger.NewServer(appFlags.BootConfig)
	subSrv := servicebase.NewSubscriberService(appFlags.BootConfig, confSrv, logSrv)

	err = subSrv.Start()
	if err != nil {
		log.Fatal(err)
	}
	defer subSrv.Stop()
	exitCode := m.Run()
	os.Exit(exitCode)
}

func TestLog(t *testing.T) {
	var err error
	connSrv := servicebase.NewConnectionService(appFlags.BootConfig)
	err = connSrv.Run(func(nc *nats.Conn) error {
		loggerProxy := logger.NewLoggerClient(nc)
		err := loggerProxy.Log(logger.LogRequest{
			App:     "testApp",
			Created: ptypes.TimestampNow(),
			Message: "test-line",
		})
		return err
	})
	if err != nil {
		t.Fatalf("Log call failed: %v", err)
	}
}

The result is a test run time as short as 1.5 seconds on an average laptop and one command allows to start test environment and test itself:

go test

PM2 and Swagger

Example ecosystem.yml configuration file for PM2 and dotnet:

apps:
  - name: 'service-name-goes-here'
    script: dotnet
    args: IO.Swagger.dll
    instances: 1

Swagger server URLs:

  1. https://localhost:5001/swagger/index.html
  2. API calls match specification path. The following is just an example: https://localhost:5001/api/path/.