sputnik

package module
v0.0.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 26, 2023 License: MIT Imports: 8 Imported by: 6

README

“Everything should be made as simple as possible...”

sputnik is tiny golang framework for building of satellite or as it's now fashionable to say sidecar processes.

GoDevGo Wiki

What do satellite processes have in common?

The same sometimes boring flow:

  • Initialize process in deterministic order
  • Connect to the server process
    • Periodically validate used connection
    • Inform about failed connection and reconnect
  • Graceful shutdown
    • Cleanup resources in deterministic order

Usually such processes are used as adapters,bridges and/or proxies for server process - they translate foreign protocol to protocol of the server.

Developers also want flexible way to create such processes without changing the code:

  • All adapters in one process
  • Adapter per process
  • Other variants

And it would be nice to write in CV that you developed modular-monolith.

sputnik to the rescue.

sputnik simplifies creation of satellite/sidecar processes for servers.

Modular monolith

sputnik forces modular-monolith design:

  • process created as set of independent asynchronously running Blocks
Satellites blueprint

sputnik supports common for all satellite processes functionality:

  • Deterministic initialization
  • Connect/Reconnect flow
  • Server heartbeat
  • Convenient negotiation between blocks of the process
  • Graceful shutdown

All this with minimal code size - actually the size of README and tests far exceeds the size of sputnik's code.

Why Sputnik?

  • Launched by the Soviet Union on 4 October 1957, Sputnik became the first satellite in space and changed the world forever.
  • Main mindset of Sputnik design was - simplicity and reliability that could be adapted to future projects
  • We were both born the same year but I'm a bit older

Less You Know, the Better You Sleep

sputnik doesn't use any server information and only assumes that server configuration and server connection are required for functioning.

Server Configuration
type ServerConfiguration any

In order to get configuration and provide it to the process, sputnik uses Configuration Factory:

type ConfFactory func(confName string, result any) error

where

  • confName - name of configuration
  • result - unmarshaled configuration(usually struct)

This function should be supplied by caller of sputnik during initialization. We will talk about initialization later.

Server Connection
type ServerConnection any

For implementation of connect/disconnect/server health flow, sputnik uses supplied by caller implementation of following interface:

type ServerConnector interface {
	// Connects to the server and return connection to server
	// If connection failed, returns error.
	// ' Connect' for already connected
	// and still not brocken connection should
	// return the same value returned in previous
	// successful call(s) and nil error
	Connect(cf ConfFactory) (conn ServerConnection, err error)

	// Returns false if
	//  - was not connected at all
	//  - was connected, but connection is brocken
	// True returned if
	//  - connected and connection is alive
	IsConnected() bool

	// If connection is alive closes it
	Disconnect()
}
Messages

sputnik supports asynchronous communication between Blocks of the process.

type Msg map[string]any

Possible types of the message:

  • command
  • query
  • event
  • update
  • .......

Developers of blocks should agree on content of messages.

sputnik doesn't force specific format of the message.

EXCEPTION: key of the map should not start from "__".

This prefix is used by sputnik for house-keeping values.

sputnik's building blocks

sputnik based process consists of infrastructure and application Blocks

Eats own dog food

Infrastructure Blocks:

  • initiator - dispatcher of all blocks
  • finisher - listener of external shutdown/exit events
  • connector - connects/reconnects with server, provides this information to another blocks
Block identity

Every Block has descriptor:

type BlockDescriptor struct {
	Name           string
	Responsibility string
}

Name of the Block should be unique. It is used for creation of the Block.

Good Block names:

  • syslogreceiver
  • syslogpublisher
  • restprocessor

Bad Block names:

  • receiver
  • processor

Remember - sputnik based process may support number of protocol adapters. And receiver usually is part of everyone.

Responsibility of the Block is used for negotiation between blocks. It's possible to create the same block with different responsibilities.

Block interface

Block has set of callbacks/hooks:

  • Mandatory:
    • Init
    • Finish
    • Run
  • Optional
    • OnServerConnect
    • OnServerDisconnect
    • OnMsg

You can see that these callbacks reflect life cycle/flow of satellite process.

Init
type Init func(cf ConfFactory) error

Init callback is executed by sputnik once during initialization. Blocks are initialized in sequenced order according to configuration.

Rules of initialization:

  • don't run hard processing within Init
  • don't work with server, wait OnServerConnect

If initialization failed (returned error != nil)

  • initialization is terminated
  • already initialized blocks are finished in opposite to init order.
Run
type Run func(communicator BlockCommunicator)

Run callback is executed by sputnik

  • after successful initialization of ALL blocks
  • on own goroutine

You can consider Run as main thread of the block.

Parameter of Run - BlockCommunicator may be used by block for negotiation with another blocks of the process.

Finish
type Finish func(init bool)

Finish callback is executed by sputnik twice:

  • during initialization of the process, if Init of another block failed (init == true)
    • for this case Finish is called synchronously, on the thread(goroutine) of initialization
  • during shutdown of the process
    • for this case Finish is called asynchronously on own goroutine

For any case, during Finish block

  • should clean all resources
  • stop all go-routines (don't forget Run's goroutine)

After finish of all blocks Sputnik quits.

OnServerConnect
type OnServerConnect func(connection any)

Optional OnServerConnect callback is executed by sputnik

  • after start of Run
  • after successful connection to server
  • on own goroutine
OnServerDisconnect
type OnServerDisconnect func()

Optional OnServerDisconnect callback is executed by sputnik

  • after start of Run
  • when previously connected server disconnects
  • on own goroutine
OnMsg
type OnMsg func(msg Msg)

Optional OnMsg callback is executed by sputnik

  • after start of Run
  • as result of receiving Msg from another block
  • Block also can send message to itself

UNLIKE OTHER CALLBACKS, OnMsg CALLED SEQUENTIALLY ONE BY ONE FROM THE SAME DEDICATED GOROUTINE. Frankly speaking - you have the queue of messages.

Block creation

Developer supplies BlockFactory function:

type BlockFactory func() *Block

BlockFactory registered in the process via RegisterBlockFactory:

func RegisterBlockFactory(blockName string, blockFactory BlockFactory)

Use init() for this registration:

func init() { // Registration of finisher block factory
	RegisterBlockFactory(DefaultFinisherName, finisherBlockFactory)
}

Where finisherBlockFactory is :

func finisherBlockFactory() *Block {
	finisher := new(finisher)
	block := NewBlock(
		WithInit(finisher.init),
		WithRun(finisher.run),
		WithFinish(finisher.finish),
		WithOnMsg(finisher.debug))
	return block
}

You can see that factory called NewBlock function using functional options pattern:

List of options:

// Mandatory:
WithInit(f Init)
WithFinish(f Finish)
WithRun(f Run)

// Optional:
WithOnConnect(f OnServerConnect)
WithOnDisconnect(f OnServerDisconnect)
WithOnMsg(f OnMsg)

where f is related callback/hook

Block control

Block control is provided via interface BlockCommunicator. Block gets own communicator as parameter of Run.

type BlockCommunicator interface {
	//
	// Get communicator of block by block's responsibility
	// Example - get BlockCommunicator of initiator:
	// initbl, ok := bc.Communicator(sputnik.InitiatorResponsibility)
	//
	Communicator(resp string) (bc BlockCommunicator, exists bool)

	// Identification of controlled block
	Descriptor() BlockDescriptor

	// Asynchronously send message to controlled block
	// true is returned if
	//  - controlled block has OnMsg callback
	//  - recipient of messages was not cancelled
	//  - msg != nil
	Send(msg Msg) bool
}

Main usage of own BlockCommunicator:

  • get BlockCommunicator of another block
  • send message to this block

Example: initiator sends setup settings to connector:

	setupMsg := make(Msg)
	setupMsg["__connector"] = connectorPlugin
	setupMsg["__timeout"] = 10000

	connCommunicator.Send(setupMsg)

sputnik flight

Create sputnik

Use NewSputnik function for creation of sputnik. It supports following options:

WithConfFactory(cf ConfFactory)                      // Set Configuration Factory. Mandatory
WithAppBlocks(appBlocks []BlockDescriptor)           // List of descriptors for application blocks
WithBlockFactories(blkFacts BlockFactories)          // List of block factories. Optional. If was not set, used list of factories registrated during init()
WithFinisher(fbd BlockDescriptor)                    // Descriptor of finisher. Optional. If was not set, default supplied finished will be used.
WithConnector(cnt ServerConnector, to time.Duration) // Server Connector plug-in and timeout for connect/reconnect. Optional

Example: creation of sputnik for tests:

	testSputnik, _ := sputnik.NewSputnik(
		sputnik.WithConfFactory(dumbConf),
		sputnik.WithAppBlocks(blkList),
		sputnik.WithBlockFactories(tb.factories()),
		sputnik.WithConnector(&tb.conntr, tb.to),
	)
Preparing for flight

After creation of sputnik, call Prepare:

// Creates and initializes all blocks.
//
// If creation and initialization of any block failed:
//
//   - Finish is called on all already initialized blocks
//
//   - Order of finish - reversal of initialization
//
//     = Returned error describes reason of the failure
//
// Otherwise returned 2 functions for sputnik management:
//
//   - lfn - Launch of the sputnik , exit from this function will be
//     after signal for shutdown of the process  or after call of
//     second returned function (see below)
//
//   - st - ShootDown of sputnik - abort flight
func (sputnik Sputnik) Prepare() (lfn Launch, st ShootDown, err error) 

Example :

	launch, kill, err := testSputnik.Prepare()
sputnik launch

Very simple - just call returned launch function.

This call is synchronous. sputnik continues to run on current goroutine till

  • process termination (it means that launch may be last line in main)
  • call of second returned function

In order to use kill(ShootDown of sputnik) function, launch and kill should run on different go-routines.

Adding blocks to the build

For adding blocks to the build use blank imports:

import (
	// Attach blocks packages to the process:
	_ "github.com/memphisdev/memphis-protocol-adapter/pkg/syslogblocks"
)

Never Asked Questions

I'd like to create stand alone process without any server? Is it possible?

Don't use WithConnector option and sputnik will not run any server connector. But for this case your blocks should not have OnServerConnect and OnServerDisconnect callbacks.

I'd like to embed sputnik to my process? Is it possible?

Of course, supply ServerConnector for in-proc communication.

You wrote that finisher can be replaced. For what?

For example in case above, you will need to coordinate exit with code of the host.

Contributing

Feel free to report bugs and suggest improvements.

License

MIT

Documentation

Overview

Package sputnik simplifies creation of satellite/sidecar processes for servers.

Modular monolith

sputnik forces modular-monolith design:

  • process created as set of independent asynchronously running blocks

Satellites blueprint

sputnik supports common for all satellite processes functionality:

  • ordered blocks initialization
  • convenient negotiation between blocks
  • server heartbeat
  • graceful shutdown

Eats own dog food

sputnik itself consists of 3 blocks:

  • "initiator" - dispatcher of all blocks
  • "finisher" - listener of external shutdown/exit events
  • "connector" - connects/reconnects with server, provides this information to another blocks

Less You Know, the Better You Sleep

sputnik knows nothing about internals of the process. It only assumes that server configuration and connection are required for functioning. This is the reason to define server connection and configuration as any. Use type assertions for "casting" to concrete interface/implementation.

Index

Constants

View Source
const (
	InitiatorResponsibility = "initiator"

	DefaultConnectorName           = "connector"
	DefaultConnectorResponsibility = "connector"

	DefaultFinisherName           = "finisher"
	DefaultFinisherResponsibility = "finisher"
)
View Source
const DefaultConnectorTimeout = time.Second * 5
View Source
const EchoBlockName = "echo"

Variables

This section is empty.

Functions

func RegisterBlockFactory added in v0.0.2

func RegisterBlockFactory(name string, bf BlockFactory)

BlockFactory registered in the process via RegisterBlockFactory Please pay attention that panic called for any error during registration.

Use init() for registration of BlockFactory

 func init() {
		sputnik.RegisterBlockFactory("syslogPublisher", slpbFactory)
	}

func RegisterBlockFactoryInner added in v0.0.2

func RegisterBlockFactoryInner(name string, bf BlockFactory, facts BlockFactories) error

Types

type Block

type Block struct {
	// contains filtered or unexported fields
}

Simplified Block life cycle:

  • Init
  • Run
  • OnServerConnect
  • [*]OnMsg
  • OnServerDisconnect
  • Finish

After Run order of callbacks will be unpredictable.

func NewBlock added in v0.0.3

func NewBlock(opts ...BlockOption) *Block

type BlockCommunicator added in v0.0.5

type BlockCommunicator interface {
	//
	// Get communicator of block by block's responsibility
	// Example - get BlockCommunicator of initiator:
	// initbl, ok := bc.Communicator(sputnik.InitiatorResponsibility)
	//
	Communicator(resp string) (bc BlockCommunicator, exists bool)

	// Identification of controlled block
	Descriptor() BlockDescriptor

	// Asynchronously send message to controlled block
	// true is returned if
	//  - controlled block has OnMsg callback
	//  - recipient of messages was not cancelled
	//  - msg != nil
	Send(msg Msg) bool
}

BlockCommunicator provides possibility for negotiation between blocks Block gets own communicator as parameter of Run

type BlockDescriptor added in v0.0.2

type BlockDescriptor struct {
	Name           string
	Responsibility string
}

Block has Name (analog of golang type) and Responsibility (instance of specific block) This separation allows to run simultaneously blocks with the same Name. Other possibility - blocks with different name but with the same responsibility, e.g. different implementation of "finisher" depends on environment.

func ConnectorDescriptor added in v0.0.3

func ConnectorDescriptor() BlockDescriptor

func FinisherDescriptor added in v0.0.2

func FinisherDescriptor() BlockDescriptor

type BlockFactories added in v0.0.2

type BlockFactories map[string]BlockFactory

func DefaultFactories added in v0.0.2

func DefaultFactories() BlockFactories

type BlockFactory added in v0.0.2

type BlockFactory func() *Block

BlockFactory should be provided for every block in the process

func EchoBlockFactory added in v0.0.7

func EchoBlockFactory(q *kissngoqueue.Queue[Msg]) BlockFactory

func Factory added in v0.0.3

func Factory(name string) (BlockFactory, error)

type BlockOption added in v0.0.3

type BlockOption func(b *Block)

func WithFinish added in v0.0.3

func WithFinish(f Finish) BlockOption

func WithInit added in v0.0.3

func WithInit(f Init) BlockOption

func WithOnConnect added in v0.0.3

func WithOnConnect(f OnServerConnect) BlockOption

func WithOnDisconnect added in v0.0.5

func WithOnDisconnect(f OnServerDisconnect) BlockOption

func WithOnMsg added in v0.0.3

func WithOnMsg(f OnMsg) BlockOption

func WithRun added in v0.0.3

func WithRun(f Run) BlockOption

type ConfFactory added in v0.0.3

type ConfFactory func(confName string, result any) error

Configuration Factory

type DummyConnector added in v0.0.6

type DummyConnector struct {
	// contains filtered or unexported fields
}

Connector's plugin used for debugging/testing

func (*DummyConnector) Connect added in v0.0.6

func (c *DummyConnector) Connect(cf ConfFactory) (conn ServerConnection, err error)

func (*DummyConnector) Disconnect added in v0.0.6

func (c *DummyConnector) Disconnect()

func (*DummyConnector) IsConnected added in v0.0.6

func (c *DummyConnector) IsConnected() bool

func (*DummyConnector) SetState added in v0.0.6

func (c *DummyConnector) SetState(connected bool)

Allows to simulate state of the connection

type Finish

type Finish func(init bool)

Finish callback is executed by sputnik:

  • during initialization of the process if init of another block failed (init == true)
  • during shutdown of the process (init == false)

Blocks are finished in reverse of initialization order.

type Init

type Init func(cf ConfFactory) error

Block has set of the callbacks:

  • mandatory: Init|Run|Finish
  • optional: OnServerConnect|OnServerDisconnect|OnMsg

Init callback is executed by sputnik once during initialization. Blocks are initialized in sequenced order according to configuration. Some rules :

  • don't run hard processing within Init
  • don't work with server till call of OnServerConnect

type Launch added in v0.0.2

type Launch func() error

sputnik launcher

type Msg added in v0.0.2

type Msg map[string]any

Because asynchronous nature of blocks, negotiation between blocks done using 'messages' Message may be command|query|event|update|... Developers of blocks should agree on content of messages. sputnik doesn't force specific format of the message with one exception: key of the map should not start from "__". This prefix is used by sputnik for house-keeping values.

func FinishMsg added in v0.0.5

func FinishMsg() Msg

func FinishedMsg added in v0.0.5

func FinishedMsg() Msg

type OnMsg added in v0.0.2

type OnMsg func(msg Msg)

Optional OnMsg callback is executed by sputnik as result of receiving Msg. Block can send message to itself. Unlike other callbacks, OnMsg called sequentially one by one from the same goroutine.

type OnServerConnect

type OnServerConnect func(connection ServerConnection)

Optional OnServerConnect callback is executed by sputnik after successful connection to server.

type OnServerDisconnect

type OnServerDisconnect func()

Optional OnServerDisconnect callback is executed by sputnik when previously connected server disconnects.

type Run

type Run func(communicator BlockCommunicator)

After successful initialization of ALL blocks, sputnik creates goroutine and calls Run Other callbacks will be executed on another goroutines After Run block is allowed to negotiate with another blocks of the process via BlockCommunicator

type ServerConfiguration added in v0.0.4

type ServerConfiguration any

Configuration

type ServerConnection added in v0.0.4

type ServerConnection any

type ServerConnector added in v0.0.4

type ServerConnector interface {
	// Connects to the server and return connection to server
	// If connection failed, returns error.
	// ' Connect' for already connected
	// and still not brocken connection should
	// return the same value returned in previous
	// successful call(s) and nil error
	Connect(cf ConfFactory) (conn ServerConnection, err error)

	// Returns false if
	//  - was not connected at all
	//  - was connected, but connection is brocken
	// True returned if
	//  - connected and connection is alive
	IsConnected() bool

	// If connection is alive closes it
	Disconnect()
}

type ShootDown added in v0.0.2

type ShootDown func()

sputnik shooter

type Sputnik added in v0.0.3

type Sputnik struct {
	// contains filtered or unexported fields
}

func NewSputnik added in v0.0.3

func NewSputnik(opts ...SputnikOption) (*Sputnik, error)

func (Sputnik) Prepare added in v0.0.3

func (sputnik Sputnik) Prepare() (lfn Launch, st ShootDown, err error)

Creates and initializes all blocks.

If creation and initialization of any block failed:

  • Finish is called on all already initialized blocks

  • Order of finish - reversal of initialization

    = Returned error describes reason of the failure

Otherwise returned 2 functions for sputnik management:

  • lfn - Launch of the sputnik , exit from this function will be after signal for shutdown of the process or after call of second returned function (see below)

  • st - ShootDown of sputnik - abort flight

type SputnikOption added in v0.0.3

type SputnikOption func(sp *Sputnik)

func WithAppBlocks added in v0.0.3

func WithAppBlocks(appBlocks []BlockDescriptor) SputnikOption

func WithBlockFactories added in v0.0.3

func WithBlockFactories(blkFacts BlockFactories) SputnikOption

func WithConfFactory added in v0.0.3

func WithConfFactory(cf ConfFactory) SputnikOption

func WithConnector added in v0.0.3

func WithConnector(cnt ServerConnector, to time.Duration) SputnikOption

func WithFinisher added in v0.0.3

func WithFinisher(fbd BlockDescriptor) SputnikOption

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL