uhaha

package module
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2020 License: MIT Imports: 31 Imported by: 5

README

uhaha

Build Status GoDoc

High Availabilty Framework for Happy Data

Uhaha is a framework for building highly available data applications in Go. This is bascially an upgrade to my Finn project, which was good but Uhaha is gooder because Uhaha has more security features (TLS and auth tokens), customizable services, deterministic time, recalculable random numbers, simpler snapshots, a smaller network footprint, and other stuff too.

Features

  • Simple API for quickly creating a fault-tolerant cluster.
  • Deterministic monotonic time that does not drift and stays in sync with the internet.
  • APIs for building custom services such as HTTP and gRPC. Supports the Redis protocol by default, so any Redis-compatible client library will work with Uhaha.
  • Multiple examples to help jumpstart integration, including a Key-value DB, a Timeseries DB, and a Ticket Service.

Example

Below a simple example of a service for monotonically increasing tickets.

package main

import "github.com/tidwall/uhaha"

type data struct {
	Ticket int64
}

func main() {
	// Set up a uhaha configuration
	var conf uhaha.Config
	
	// Give the application a name. All servers in the cluster should use the
	// same name.
	conf.Name = "ticket"
	
	// Set the initial data. This is state of the data when first server in the 
	// cluster starts for the first time ever.
	conf.InitialData = new(data)

	// Since we are not holding onto much data we can used the built-in JSON
	// snapshot system. You just need to make sure all the important fields in
	// the data are exportable (capitalized) to JSON. In this case there is
	// only the one field "Ticket".
	conf.UseJSONSnapshots = true
	
	// Add a command that will change the value of a Ticket. 
	conf.AddWriteCommand("ticket", cmdTICKET)

	// Finally, hand off all processing to uhaha.
	uhaha.Main(conf)
}

// TICKET
// help: returns a new ticket that has a value that is at least one greater
// than the previous TICKET call.
func cmdTICKET(m uhaha.Machine, args []string) (interface{}, error) {
	// The the current data from the machine
	data := m.Data().(*data)

	// Increment the ticket
	data.Ticket++

	// Return the new ticket to caller
	return data.Ticket, nil
}

Building

Using the source file from the examples directory, we'll build an application named "ticket"

go build -o ticket examples/ticket/main.go

Running

It's ideal to have three, five, or seven nodes in your cluster.

Let's create the first node.

./ticket -n 1 -a :11001

This will create a node named 1 and bind the address to :11001

Now let's create two more nodes and add them to the cluster.

./ticket -n 2 -a :11002 -j :11001
./ticket -n 3 -a :11003 -j :11001

Now we have a fault-tolerant three node cluster up and running.

Using

You can use any Redis compatible client, such as the redis-cli, telnet, or netcat.

I'll use the redis-cli in the example below.

Connect to the leader. This will probably be the first node you created.

redis-cli -p 11001

Send the server a TICKET command and receive the first ticket.

> TICKET
"1"

From here on every TICKET command will guarentee to generate a value larger than the previous TICKET command.

> TICKET
"2"
> TICKET
"3"
> TICKET
"4"
> TICKET
"5"

Built-in Commands

There are a number built-in commands for managing and monitor the cluster.

MACHINE                                 # show information about the machine
RAFT LEADER                             # show the address of the current raft leader
RAFT INFO [pattern]                     # show information about the raft server and cluster
RAFT SERVER LIST                        # show all servers in cluster
RAFT SERVER ADD id address              # add a server to cluster
RAFT SERVER REMOVE id                   # remove a server from the cluster
RAFT SNAPSHOT NOW                       # make a snapshot of the data
RAFT SNAPSHOT LIST                      # show a list of all snapshots on server
RAFT SNAPSHOT FILE id                   # show the file path of a snapshot on server
RAFT SNAPSHOT READ id [RANGE start end] # download all or part of a snapshot

Command-line options

my-uhaha-app version: 0.0.0

Usage: my-uhaha-app [-n id] [-a addr] [options]

Basic options:
  -v              : display version
  -h              : display help, this screen
  -a addr         : bind to address  (default: 127.0.0.1:11001)
  -n id           : node ID  (default: 1)
  -d dir          : data directory  (default: data)
  -j addr         : leader address of a cluster to join
  -l level        : log level  (default: notice) [debug,verb,notice,warn,silent]

Security options:
  --tls-cert path : path to TLS certificate
  --tls-key path  : path to TLS private key
  --tls-ca path   : path to CA certificate to be used as a trusted root when 
                    validating certificates.
  --auth auth     : cluster authorization, shared by all servers and clients

Advanced options:
  --nosync        : turn off syncing data to disk after every write. This leads
                    to faster write operations but opens up the chance for data
                    loss due to catastrophic events such as power failure.
  --openreads     : allow followers to process read commands, but with the 
                    possibility of returning stale data.
  --localtime     : have the raft machine time synchronized with the local
                    server rather than the public internet. This will run the 
                    risk of time shifts when the local server time is
                    drastically changed during live operation. 
  --restore path  : restore a raft machine from a snapshot file. This will
                    start a brand new single-node cluster using the snapshot as
                    initial data. The other nodes must be re-joined. This
                    operation is ignored when a data directory already exists.
                    Cannot be used with -j flag.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCorrupt = errors.New("corrupt")

ErrCorrupt is returned when a data is invalid or corrupt

View Source
var ErrInvalid = errors.New("invalid")

ErrInvalid is returned when an operation has invalid arguments or options

View Source
var ErrSyntax = errors.New("syntax error")

ErrSyntax is returned where there was a syntax error

View Source
var ErrUnauthorized = errors.New("unauthorized")

ErrUnauthorized is returned when a client connection has not been authorized

View Source
var ErrUnknownCommand = errors.New("unknown command")

ErrUnknownCommand is returned when a command is not known

View Source
var ErrWrongNumArgs = errors.New("wrong number of arguments")

ErrWrongNumArgs is returned when the arg count is wrong

Functions

func Main

func Main(conf Config)

Main entrypoint for the cluster node. This must be called once and only once, and as the last call in the Go main() function. There are no return values as all application operations, logging, and I/O will be forever transferred.

func ReadRawMachineInfo added in v0.1.5

func ReadRawMachineInfo(m Machine, info *RawMachineInfo)

ReadRawMachineInfo reads the raw machine components.

func WriteRawMachineInfo added in v0.1.5

func WriteRawMachineInfo(m Machine, info *RawMachineInfo)

WriteRawMachineInfo writes raw components to the machine. Use with care as this operation may destroy the consistency of your cluster.

Types

type Backend

type Backend int

The Backend database format used for storing Raft logs and meta data.

const (
	// LevelDB is an on-disk LSM (LSM log-structured merge-tree) database. This
	// format is optimized for fast sequential reads and writes, which is ideal
	// for most Raft implementations. This is the default format used by Uhaha.
	LevelDB Backend = iota
	// Bolt is an on-disk single-file b+tree database. This format has been a
	// popular choice for Go-based Raft implementations for years.
	Bolt
)

type Config

type Config struct {

	// Name gives the server application a name. Default "uhaha-app"
	Name string

	// Version of the application. Default "0.0.0"
	Version string

	// Flag is used to manage the application startup flags.
	Flag struct {
		// Custom tells Main to not automatically parse the application startup
		// flags. When set it is up to the user to parse the os.Args manually
		// or with a different library.
		Custom bool
		// Usage is an optional function that allows for altering the usage
		// message.
		Usage func(usage string) string
		// PreParse is an optional function that allows for adding command line
		// flags before the user flags are parsed.
		PreParse func()
		// PostParse is an optional function that fires after user flags are
		// parsed.
		PostParse func()
	}

	// Snapshot fires when a snapshot
	Snapshot func(data interface{}) (Snapshot, error)

	// Restore returns a data object that is fully restored from the previous
	// snapshot using the input Reader. A restore operation on happens once,
	// if needed, at the start of the application.
	Restore func(rd io.Reader) (data interface{}, err error)

	// UseJSONSnapshots is a convienence field that tells the machine to use
	// JSON as the format for all snapshots and restores. This may be good for
	// small simple data models which have types that can be fully marshalled
	// into JSON, ie. all imporant data fields need to exportable (Capitalized).
	// For more complicated or specialized data, it's proabably best to assign
	// custom functions to the Config.Snapshot and Config.Restore fields.
	// It's invalid to set this field while also setting Snapshot and/or
	// Restore. Default false
	UseJSONSnapshots bool

	// Tick fires at regular intervals as specified by TickDelay. This function
	// can be used to make updates to the database.
	Tick func(m Machine)

	// DataDirReady is an optional callback function that fires containing the
	// path to the directory where all the logs and snapshots are stored.
	DataDirReady func(dir string)

	LocalTime     bool          // default false
	TickDelay     time.Duration // default 200ms
	BackupPath    string        // default ""
	InitialData   interface{}   // default nil
	NodeID        string        // default "1"
	Addr          string        // default ":11001"
	DataDir       string        // default "data"
	LogOutput     io.Writer     // default os.Stderr
	LogLevel      string        // default "notice"
	JoinAddr      string        // default ""
	Backend       Backend       // default LevelDB
	NoSync        bool          // default false
	OpenReads     bool          // default false
	MaxPool       int           // default 8
	TLSCertPath   string        // default ""
	TLSKeyPath    string        // default ""
	TLSCACertPath string        // default ""
	Auth          string        // default ""
	// contains filtered or unexported fields
}

Config is the configuration for managing the behavior of the application. This must be fill out prior and then passed to the uhaha.Main() function.

func (*Config) AddPassiveCommand

func (conf *Config) AddPassiveCommand(name string,
	fn func(m Machine, args []string) (interface{}, error),
)

AddPassiveCommand adds a command that is for peforming client and system specific operations. It *is not* intended for working with the machine data, and doing so will risk data corruption.

func (*Config) AddReadCommand

func (conf *Config) AddReadCommand(name string,
	fn func(m Machine, args []string) (interface{}, error),
)

AddReadCommand adds a command for reading machine data.

func (*Config) AddService

func (conf *Config) AddService(sniff func(rd io.Reader) bool,
	acceptor func(s Service, ln net.Listener),
)

AddService adds a custom client network service, such as HTTP or gRPC. By default, a Redis compatible service is already included.

func (*Config) AddWriteCommand

func (conf *Config) AddWriteCommand(name string,
	fn func(m Machine, args []string) (interface{}, error),
)

AddWriteCommand adds a command for reading or altering machine data.

type FilterArgs added in v0.1.1

type FilterArgs []string

FilterArgs ...

type Hijack

type Hijack func(s Service, conn HijackedConn)

Hijack is a function type that can be used to "hijack" a service client connection and allowing to perform I/O operations outside the standard network loop. An example of it's usage can be found in the examples/kvdb project.

type HijackedConn

type HijackedConn interface {
	// RemoteAddr is the connection remote tcp address.
	RemoteAddr() string
	// ReadCommands is an iterator function that reads pipelined commands.
	// Returns a error when the connection encountared and error.
	ReadCommands(func(args []string) bool) error
	// ReadCommand reads one command at a time.
	ReadCommand() (args []string, err error)
	// WriteAny writes any type to the write buffer using the format rules that
	// are defined by the original Service.
	WriteAny(v interface{})
	// WriteRaw writes raw data to the write buffer.
	WriteRaw(data []byte)
	// Flush the write write buffer and send data to the connection.
	Flush() error
	// Close the connection
	Close() error
}

HijackedConn is a connection that has been detached from the main service network loop. It's entirely up to the hijacker to performs all I/O operations. The Write* functions buffer write data and the Flush must be called to do the actual sending of the data to the connection. Close the connection to when done.

type Logger

type Logger interface {
	Debugf(format string, args ...interface{})
	Debug(args ...interface{})
	Debugln(args ...interface{})
	Verbf(format string, args ...interface{})
	Verb(args ...interface{})
	Verbln(args ...interface{})
	Noticef(format string, args ...interface{})
	Notice(args ...interface{})
	Noticeln(args ...interface{})
	Printf(format string, args ...interface{})
	Print(args ...interface{})
	Println(args ...interface{})
	Warningf(format string, args ...interface{})
	Warning(args ...interface{})
	Warningln(args ...interface{})
	Fatalf(format string, args ...interface{})
	Fatal(args ...interface{})
	Fatalln(args ...interface{})
	Panicf(format string, args ...interface{})
	Panic(args ...interface{})
	Panicln(args ...interface{})
	Errorf(format string, args ...interface{})
	Error(args ...interface{})
	Errorln(args ...interface{})
}

Logger is the logger used by Uhaha for printing all console messages.

type Machine

type Machine interface {
	// Data is the original user data interface that was assigned at startup.
	// It's safe to alter the data in this interface while inside a Write
	// command, but it's only safe to read from this interface for Read and
	// Passive commands.
	Data() interface{}
	// Now generates a stable timestamp that is synced with internet time
	// and for Write commands is always monotonical increasing. It's made to
	// be a trusted source of time for performing operations on the user data.
	// Always use this function instead of the builtin time.Now().
	Now() time.Time
	// Rand is a random number generator that must be used instead of the
	// standard Go packages `crypto/rand` and `math/rand`. For Write commands
	// the values returned from this generator are crypto seeded, guaranteed
	// to be reproduced in exact order when the server restarts, and identical
	// across all machines in the cluster. The underlying implementation is
	// PCG. Check out http://www.pcg-random.org/ for more information.
	Rand() Rand
	// Utility logger for printing information to the local server log.
	Log() Logger
}

The Machine interface is passed to every command. It includes the user data and various utilities that should be used from Write, Read, and Passive commands.

It's important to note that the Rand() and Now() functions can be used safely in Write, Read, and Passive commands. But, that each call to Rand() and Now() from inside of a Read/Passive command will always return back the same last known value of it's respective type. While, from a Write command, you'll get freshly generated values. This is to ensure that the every single command ALWAYS generates the same series of data on every server.

type Message

type Message struct {
	// Args are the original command arguments.
	Args []string
	// Resp is the command reponse, if not an error.
	Resp interface{}
	// Err is the command error, if not successful.
	Err error
	// Elapsed is the amount of time that the command took to process.
	Elapsed time.Duration
	// Addr is the remote TCP address of the connection that generated
	// this message.
	Addr string
}

A Message represents a command and is in a format that is consumed by an Observer.

type Monitor

type Monitor interface {
	// Send a message to observers
	Send(msg Message)
	// NewObjser returns a new Observer containing a channel that will send the
	// messages for every command processed by the service.
	// Stop the observer to release associated resources.
	NewObserver() Observer
}

Monitor represents an interface for sending and consuming command messages that are processed by a Service.

type Observer

type Observer interface {
	Stop()
	C() <-chan Message
}

An Observer holds a channel that delivers the messages for all commands processed by a Service.

type Rand

type Rand interface {
	Int() int
	Uint64() uint64
	Uint32() uint32
	Float64() float64
	Read([]byte) (n int, err error)
}

Rand is a random number interface used by Machine

type RawMachineInfo added in v0.1.5

type RawMachineInfo struct {
	TS   int64
	Seed int64
}

RawMachineInfo represents the raw components of the machine

type Receiver

type Receiver interface {
	Recv() (interface{}, time.Duration, error)
}

Receiver ...

func Response

func Response(v interface{}, elapsed time.Duration, err error) Receiver

Response ...

type SendOptions

type SendOptions struct {
	From           interface{}
	AllowOpenReads bool
	DenyOpenReads  bool
}

SendOptions ...

type Service

type Service interface {
	// Send a command with args from a client
	Send(args []string, opts *SendOptions) Receiver
	// Auth authorizes a client
	Auth(auth string) error
	// Log is shared logger
	Log() Logger
	// Monitor returns a service monitor for observing client commands.
	Monitor() Monitor
}

Service is a client facing service.

type Snapshot

type Snapshot interface {
	Persist(io.Writer) error
	Done(path string)
}

A Snapshot is an interface that allows for Raft snapshots to be taken.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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