finn

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2019 License: MIT Imports: 18 Imported by: 5

README

FINN

Build Status Go Report Card GoDoc

Finn is a fast and simple framework for building Raft implementations in Go. It uses Redcon for the network transport and Hashicorp Raft. There is also the option to use LevelDB, BoltDB or FastLog for log persistence.

The reason for this project is to add Raft support to a future release of BuntDB and Tile38.

Features

Getting Started

Installing

To start using Finn, install Go and run go get:

$ go get -u github.com/tidwall/finn

This will retrieve the library.

Example

Here's an example of a Redis clone that accepts the GET, SET, DEL, and KEYS commands.

You can run a full-featured version of this example from a terminal:

go run example/clone.go
package main

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"sort"
	"strings"
	"sync"

	"github.com/tidwall/finn"
	"github.com/tidwall/match"
	"github.com/tidwall/redcon"
)

func main() {
	n, err := finn.Open("data", ":7481", "", NewClone(), nil)
	if err != nil {
		log.Fatal(err)
	}
	defer n.Close()
	select {}
}

type Clone struct {
	mu   sync.RWMutex
	keys map[string][]byte
}

func NewClone() *Clone {
	return &Clone{keys: make(map[string][]byte)}
}

func (kvm *Clone) Command(m finn.Applier, conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	switch strings.ToLower(string(cmd.Args[0])) {
	default:
		return nil, finn.ErrUnknownCommand
	case "set":
		if len(cmd.Args) != 3 {
			return nil, finn.ErrWrongNumberOfArguments
		}
		return m.Apply(conn, cmd,
			func() (interface{}, error) {
				kvm.mu.Lock()
				kvm.keys[string(cmd.Args[1])] = cmd.Args[2]
				kvm.mu.Unlock()
				return nil, nil
			},
			func(v interface{}) (interface{}, error) {
				conn.WriteString("OK")
				return nil, nil
			},
		)
	case "get":
		if len(cmd.Args) != 2 {
			return nil, finn.ErrWrongNumberOfArguments
		}
		return m.Apply(conn, cmd, nil,
			func(interface{}) (interface{}, error) {
				kvm.mu.RLock()
				val, ok := kvm.keys[string(cmd.Args[1])]
				kvm.mu.RUnlock()
				if !ok {
					conn.WriteNull()
				} else {
					conn.WriteBulk(val)
				}
				return nil, nil
			},
		)
	case "del":
		if len(cmd.Args) < 2 {
			return nil, finn.ErrWrongNumberOfArguments
		}
		return m.Apply(conn, cmd,
			func() (interface{}, error) {
				var n int
				kvm.mu.Lock()
				for i := 1; i < len(cmd.Args); i++ {
					key := string(cmd.Args[i])
					if _, ok := kvm.keys[key]; ok {
						delete(kvm.keys, key)
						n++
					}
				}
				kvm.mu.Unlock()
				return n, nil
			},
			func(v interface{}) (interface{}, error) {
				n := v.(int)
				conn.WriteInt(n)
				return nil, nil
			},
		)
	case "keys":
		if len(cmd.Args) != 2 {
			return nil, finn.ErrWrongNumberOfArguments
		}
		pattern := string(cmd.Args[1])
		return m.Apply(conn, cmd, nil,
			func(v interface{}) (interface{}, error) {
				var keys []string
				kvm.mu.RLock()
				for key := range kvm.keys {
					if match.Match(key, pattern) {
						keys = append(keys, key)
					}
				}
				kvm.mu.RUnlock()
				sort.Strings(keys)
				conn.WriteArray(len(keys))
				for _, key := range keys {
					conn.WriteBulkString(key)
				}
				return nil, nil
			},
		)
	}
}

func (kvm *Clone) Restore(rd io.Reader) error {
	kvm.mu.Lock()
	defer kvm.mu.Unlock()
	data, err := ioutil.ReadAll(rd)
	if err != nil {
		return err
	}
	var keys map[string][]byte
	if err := json.Unmarshal(data, &keys); err != nil {
		return err
	}
	kvm.keys = keys
	return nil
}

func (kvm *Clone) Snapshot(wr io.Writer) error {
	kvm.mu.RLock()
	defer kvm.mu.RUnlock()
	data, err := json.Marshal(kvm.keys)
	if err != nil {
		return err
	}
	if _, err := wr.Write(data); err != nil {
		return err
	}
	return nil
}

The Applier Type

Every Command() call provides an Applier type which is responsible for handling all Read or Write operation. In the above example you will see one m.Apply(conn, cmd, ...) for each command.

The signature for the Apply() function is:

func Apply(
	conn redcon.Conn, 
	cmd redcon.Command,
	mutate func() (interface{}, error),
	respond func(interface{}) (interface{}, error),
) (interface{}, error)
  • conn is the client connection making the call. It's possible that this value may be nil for commands that are being replicated on Follower nodes.
  • cmd is the command to process.
  • mutate is the function that handles modifying the node's data. Passing nil indicates that the operation is read-only. The interface{} return value will be passed to the respond func. Returning an error will cancel the operation and the error will be returned to the client.
  • respond is used for responding to the client connection. It's also used for read-only operations. The interface{} param is what was passed from the mutate function and may be nil. Returning an error will cancel the operation and the error will be returned to the client.

Please note that the Apply() command is required for modifying or accessing data that is shared on all of the nodes. Optionally you can forgo the call altogether for operations that are unique to the node.

Snapshots

All Raft commands are stored in one big log file that will continue to grow. The log is stored on disk, in memory, or both. At some point the server will run out of memory or disk space. Snapshots allows for truncating the log so that it does not take up all of the server's resources.

The two functions Snapshot and Restore are used to create a snapshot and restore a snapshot, respectively.

The Snapshot() function passes a writer that you can write your snapshot to. Return nil to indicate that you are done writing. Returning an error will cancel the snapshot. If you want to disable snapshots altogether:

func (kvm *Clone) Snapshot(wr io.Writer) error {
	return finn.ErrDisabled
}

The Restore() function passes a reader that you can use to restore your snapshot from.

Please note that the Raft cluster is active during a snapshot operation. In the example above we use a read-lock that will force the cluster to delay all writes until the snapshot is complete. This may not be ideal for your scenario.

There's a command line Redis clone that supports all of Finn's features. Print the help options:

go run example/clone.go -h

First start a single-member cluster:

go run example/clone.go

This will start the clone listening on port 7481 for client and server-to-server communication.

Next, let's set a single key, and then retrieve it:

$ redis-cli -p 7481 SET mykey "my value"
OK
$ redis-cli -p 7481 GET mykey
"my value"

Adding members:

go run example/clone.go -p 7482 -dir data2 -join 7481
go run example/clone.go -p 7483 -dir data3 -join 7481

That's it. Now if node1 goes down, node2 and node3 will continue to operate.

Built-in Raft Commands

Here are a few commands for monitoring and managing the cluster:

  • RAFTADDPEER addr
    Adds a new member to the Raft cluster
  • RAFTREMOVEPEER addr
    Removes an existing member
  • RAFTPEERS addr
    Lists known peers and their status
  • RAFTLEADER
    Returns the Raft leader, if known
  • RAFTSNAPSHOT
    Triggers a snapshot operation
  • RAFTSTATE
    Returns the state of the node
  • RAFTSTATS
    Returns information and statistics for the node and cluster

Consistency and Durability

Write Durability

The Options.Durability field has the following options:

  • Low - fsync is managed by the operating system, less safe
  • Medium - fsync every second, fast and safer
  • High - fsync after every write, very durable, slower

Read Consistency

The Options.Consistency field has the following options:

  • Low - all nodes accept reads, small risk of stale data
  • Medium - only the leader accepts reads, itty-bitty risk of stale data during a leadership change
  • High - only the leader accepts reads, the raft log index is incremented to guaranteeing no stale data

For example, setting the following options:

opts := finn.Options{
	Consistency: High,
	Durability: High,
}
n, err := finn.Open("data", ":7481", "", &opts)

Provides the highest level of durability and consistency.

Log Backends

Finn supports the following log databases.

  • FastLog - log is stored in memory and persists to disk, very fast reads and writes, log is limited to the amount of server memory.
  • LevelDB - log is stored only to disk, supports large logs.
  • Bolt - log is stored only to disk, supports large logs.

Contact

Josh Baker @tidwall

License

Finn source code is available under the MIT License.

Documentation

Overview

Package finn provide a fast and simple Raft implementation.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownCommand is returned when the command is not known.
	ErrUnknownCommand = errors.New("unknown command")
	// ErrWrongNumberOfArguments is returned when the number of arguments is wrong.
	ErrWrongNumberOfArguments = errors.New("wrong number of arguments")
	// ErrDisabled is returned when a feature is disabled.
	ErrDisabled = errors.New("disabled")
)

Functions

This section is empty.

Types

type Applier

type Applier interface {
	// Apply applies a command
	Apply(conn redcon.Conn, cmd redcon.Command,
		mutate func() (interface{}, error),
		respond func(interface{}) (interface{}, error),
	) (interface{}, error)
	Log() Logger
}

Applier is used to apply raft commands.

type Backend

type Backend int

Backend is a raft log database type.

const (
	// FastLog is a persistent in-memory raft log.
	// This is the default.
	FastLog Backend = iota
	// Bolt is a persistent disk raft log.
	Bolt
	// InMem is a non-persistent in-memory raft log.
	InMem
	// LevelDB is a persistent disk raft log.
	LevelDB
)

func (Backend) String

func (b Backend) String() string

String returns a string representation of the Backend

type Level

type Level int

Level is for defining the raft consistency level.

const (
	// Low is "low" consistency. All readonly commands will can processed by
	// any node. Very fast but may have stale reads.
	Low Level = -1
	// Medium is "medium" consistency. All readonly commands can only be
	// processed by the leader. The command is not processed through the
	// raft log, therefore a very small (microseconds) chance for a stale
	// read is possible when a leader change occurs. Fast but only the leader
	// handles all reads and writes.
	Medium Level = 0
	// High is "high" consistency. All commands go through the raft log.
	// Not as fast because all commands must pass through the raft log.
	High Level = 1
)

func (Level) String

func (l Level) String() string

String returns a string representation of Level.

type LogLevel

type LogLevel int

LogLevel is used to define the verbosity of the log outputs

const (
	// Debug prints everything
	Debug LogLevel = -2
	// Verbose prints extra detail
	Verbose LogLevel = -1
	// Notice is the standard level
	Notice LogLevel = 0
	// Warning only prints warnings
	Warning LogLevel = 1
)

type Logger

type Logger interface {
	// Printf write notice messages
	Printf(format string, args ...interface{})
	// Verbosef writes verbose messages
	Verbosef(format string, args ...interface{})
	// Noticef writes notice messages
	Noticef(format string, args ...interface{})
	// Warningf write warning messages
	Warningf(format string, args ...interface{})
	// Debugf writes debug messages
	Debugf(format string, args ...interface{})
}

Logger is a logger

type Machine

type Machine interface {
	// Command is called by the Node for incoming commands.
	Command(a Applier, conn redcon.Conn, cmd redcon.Command) (interface{}, error)
	// Restore is used to restore data from a snapshot.
	Restore(rd io.Reader) error
	// Snapshot is used to support log compaction. This call should write a
	// snapshot to the provided writer.
	Snapshot(wr io.Writer) error
}

Machine handles raft commands and raft snapshotting.

type Node

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

Node represents a Raft server node.

func Open

func Open(dir, addr, join string, handler Machine, opts *Options) (node *Node, err error)

Open opens a Raft node and returns the Node to the caller.

func (*Node) Close

func (n *Node) Close() error

Close closes the node

func (*Node) Log

func (n *Node) Log() Logger

Log returns the active logger for printing messages

func (*Node) Store

func (n *Node) Store() interface{}

Store returns the underlying storage object.

type Options

type Options struct {
	// Consistency is the raft consistency level for reads.
	// Default is Medium
	Consistency Level
	// Durability is the fsync durability for disk writes.
	// Default is Medium
	Durability Level
	// Backend is the database backend.
	// Default is MemLog
	Backend Backend
	// LogLevel is the log verbosity
	// Default is Notice
	LogLevel LogLevel
	// LogOutput is the log writer
	// Default is os.Stderr
	LogOutput io.Writer
	// Accept is an optional function that can be used to
	// accept or deny a connection. It fires when new client
	// connections are created.
	// Return false to deny the connection.
	ConnAccept func(redcon.Conn) bool
	// ConnClosed is an optional function that fires
	// when client connections are closed.
	// If there was a network error, then the error will be
	// passed in as an argument.
	ConnClosed func(redcon.Conn, error)
}

Options are used to provide a Node with optional functionality.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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