Full Code of tidwall/finn for AI

master e430290cc478 cached
7 files
61.2 KB
20.7k tokens
93 symbols
1 requests
Download .txt
Repository: tidwall/finn
Branch: master
Commit: e430290cc478
Files: 7
Total size: 61.2 KB

Directory structure:
gitextract_cfjio72r/

├── LICENSE
├── README.md
├── example/
│   └── clone.go
├── finn.go
├── finn_test.go
├── go.mod
└── go.sum

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Josh Baker

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
**This project has been archived. Please check out [Uhaha](https://github.com/tidwall/uhaha) for a fitter, happier, more productive Raft framework.**

<p align="center">
<img 
    src="logo.jpg" 
    width="314" height="200" border="0" alt="FINN">
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/tidwall/finn"><img src="https://goreportcard.com/badge/github.com/tidwall/finn?style=flat-square" alt="Go Report Card"></a>
<a href="https://godoc.org/github.com/tidwall/finn"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
</p>

Finn is a fast and simple framework for building [Raft](https://raft.github.io/) implementations in Go. It uses [Redcon](https://github.com/tidwall/redcon) for the network transport and [Hashicorp Raft](https://github.com/hashicorp/raft). There is also the option to use [LevelDB](https://github.com/syndtr/goleveldb), [BoltDB](https://github.com/boltdb/bolt) or [FastLog](https://github.com/tidwall/raft-fastlog) for log persistence.


Features
--------

- Simple API for quickly creating a [fault-tolerant](https://en.wikipedia.org/wiki/Fault_tolerance) cluster
- Fast network protocol using the [raft-redcon](https://github.com/tidwall/raft-redcon) transport
- Optional [backends](#log-backends) for log persistence. [LevelDB](https://github.com/syndtr/goleveldb), [BoltDB](https://github.com/boltdb/bolt), or [FastLog](https://github.com/tidwall/raft-fastlog)
- Adjustable [consistency and durability](#consistency-and-durability) levels
- A [full-featured example](#full-featured-example) to help jumpstart integration
- [Built-in raft commands](#built-in-raft-commands) for monitoring and managing the cluster
- Supports the [Redis log format](http://build47.com/redis-log-format-levels/)
- Works with clients such as [redigo](https://github.com/garyburd/redigo), [redis-py](https://github.com/andymccurdy/redis-py), [node_redis](https://github.com/NodeRedis/node_redis), [jedis](https://github.com/xetorthio/jedis), and [redis-cli](http://redis.io/topics/rediscli)


Getting Started
---------------

### Installing

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

```sh
$ 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](#full-featured-example) of this example from a terminal:

```
go run example/clone.go
```

```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:
```go
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:

```go
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.*

Full-featured Example
---------------------

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](http://stackoverflow.com/questions/1563319/what-is-stale-state) 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:

```go
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](https://github.com/tidwall/raft-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](https://github.com/syndtr/goleveldb) - log is stored only to disk, supports large logs.
- [Bolt](https://github.com/boltdb/bolt) - log is stored only to disk, supports large logs.

Contact
-------
Josh Baker [@tidwall](http://twitter.com/tidwall)

License
-------
Finn source code is available under the MIT [License](/LICENSE).


================================================
FILE: example/clone.go
================================================
package main

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

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

func main() {
	var port int
	var backend string
	var durability string
	var consistency string
	var loglevel string
	var join string
	var dir string

	flag.IntVar(&port, "p", 7481, "Bind port")
	flag.StringVar(&backend, "backend", "fastlog", "Raft log backend [fastlog,bolt,inmem]")
	flag.StringVar(&durability, "durability", "medium", "Log durability [low,medium,high]")
	flag.StringVar(&consistency, "consistency", "medium", "Raft consistency [low,medium,high]")
	flag.StringVar(&loglevel, "loglevel", "notice", "Log level [quiet,warning,notice,verbose,debug]")
	flag.StringVar(&dir, "dir", "data", "Data directory")
	flag.StringVar(&join, "join", "", "Join a cluster by providing an address")
	flag.Parse()

	var opts finn.Options

	switch strings.ToLower(backend) {
	default:
		log.Fatalf("invalid backend '%v'", backend)
	case "fastlog":
		opts.Backend = finn.FastLog
	case "bolt":
		opts.Backend = finn.Bolt
	case "inmem":
		opts.Backend = finn.InMem
	}
	switch strings.ToLower(durability) {
	default:
		log.Fatalf("invalid durability '%v'", durability)
	case "low":
		opts.Durability = finn.Low
	case "medium":
		opts.Durability = finn.Medium
	case "high":
		opts.Durability = finn.High
	}
	switch strings.ToLower(consistency) {
	default:
		log.Fatalf("invalid consistency '%v'", consistency)
	case "low":
		opts.Consistency = finn.Low
	case "medium":
		opts.Consistency = finn.Medium
	case "high":
		opts.Consistency = finn.High
	}
	switch strings.ToLower(loglevel) {
	default:
		log.Fatalf("invalid loglevel '%v'", loglevel)
	case "quiet":
		opts.LogOutput = ioutil.Discard
	case "warning":
		opts.LogLevel = finn.Warning
	case "notice":
		opts.LogLevel = finn.Notice
	case "verbose":
		opts.LogLevel = finn.Verbose
	case "debug":
		opts.LogLevel = finn.Debug
	}
	n, err := finn.Open(dir, fmt.Sprintf(":%d", port), join, NewClone(), &opts)
	if err != nil {
		if opts.LogOutput == ioutil.Discard {
			log.Fatal(err)
		}
	}
	defer n.Close()
	select {}
}

// Clone represent a Redis clone machine
type Clone struct {
	mu   sync.RWMutex
	keys map[string][]byte
}

// NewClone create a new clone
func NewClone() *Clone {
	return &Clone{
		keys: make(map[string][]byte),
	}
}

// Command processes a command
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
			},
		)
	}
}

// Restore restores a snapshot
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
}

// Snapshot creates a snapshot
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
}


================================================
FILE: finn.go
================================================
// Package finn provide a fast and simple Raft implementation.
package finn

import (
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/hashicorp/raft"
	raftboltdb "github.com/tidwall/raft-boltdb"
	raftfastlog "github.com/tidwall/raft-fastlog"
	raftleveldb "github.com/tidwall/raft-leveldb"
	raftredcon "github.com/tidwall/raft-redcon"
	"github.com/tidwall/redcon"
	"github.com/tidwall/redlog"
)

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")
)

var (
	errInvalidCommand          = errors.New("invalid command")
	errInvalidConsistencyLevel = errors.New("invalid consistency level")
	errSyntaxError             = errors.New("syntax error")
	errInvalidResponse         = errors.New("invalid response")
)

const (
	retainSnapshotCount = 2
	raftTimeout         = 10 * time.Second
)

// Level is for defining the raft consistency level.
type Level int

// String returns a string representation of Level.
func (l Level) String() string {
	switch l {
	default:
		return "unknown"
	case Low:
		return "low"
	case Medium:
		return "medium"
	case High:
		return "high"
	}
}

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
)

// Backend is a raft log database type.
type Backend int

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
)

// String returns a string representation of the Backend
func (b Backend) String() string {
	switch b {
	default:
		return "unknown"
	case FastLog:
		return "fastlog"
	case Bolt:
		return "bolt"
	case InMem:
		return "inmem"
	case LevelDB:
		return "leveldb"
	}
}

// LogLevel is used to define the verbosity of the log outputs
type LogLevel int

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
)

// Options are used to provide a Node with optional functionality.
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)
}

// fillOptions fills in default options
func fillOptions(opts *Options) *Options {
	if opts == nil {
		opts = &Options{}
	}
	// copy and reassign the options
	nopts := *opts
	if nopts.LogOutput == nil {
		nopts.LogOutput = os.Stderr
	}
	return &nopts
}

// Logger is a 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{})
}

// Applier is used to apply raft commands.
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
}

// Machine handles raft commands and raft snapshotting.
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
}

// Node represents a Raft server node.
type Node struct {
	mu       sync.RWMutex
	addr     string
	snapshot raft.SnapshotStore
	trans    *raftredcon.RedconTransport
	raft     *raft.Raft
	log      *redlog.Logger // the node logger
	mlog     *redlog.Logger // the machine logger
	closed   bool
	opts     *Options
	level    Level
	handler  Machine
	store    bigStore
	peers    map[string]string
}

// bigStore represents a raft store that conforms to
// raft.PeerStore, raft.LogStore, and raft.StableStore.
type bigStore interface {
	Close() error
	FirstIndex() (uint64, error)
	LastIndex() (uint64, error)
	GetLog(idx uint64, log *raft.Log) error
	StoreLog(log *raft.Log) error
	StoreLogs(logs []*raft.Log) error
	DeleteRange(min, max uint64) error
	Set(k, v []byte) error
	Get(k []byte) ([]byte, error)
	SetUint64(key []byte, val uint64) error
	GetUint64(key []byte) (uint64, error)
	Peers() ([]string, error)
	SetPeers(peers []string) error
}

// Open opens a Raft node and returns the Node to the caller.
func Open(dir, addr, join string, handler Machine, opts *Options) (node *Node, err error) {
	opts = fillOptions(opts)
	log := redlog.New(opts.LogOutput).Sub('N')
	log.SetFilter(redlog.HashicorpRaftFilter)
	log.SetIgnoreDups(true)
	switch opts.LogLevel {
	case Debug:
		log.SetLevel(0)
	case Verbose:
		log.SetLevel(1)
	case Notice:
		log.SetLevel(2)
	case Warning:
		log.SetLevel(3)
	}

	// if this function fails then write the error to the logger
	defer func() {
		if err != nil {
			log.Warningf("%v", err)
		}
	}()

	// create the directory
	if err := os.MkdirAll(dir, 0700); err != nil {
		return nil, err
	}

	// create a node and assign it some fields
	n := &Node{
		log:     log,
		mlog:    log.Sub('C'),
		opts:    opts,
		level:   opts.Consistency,
		handler: handler,
		peers:   make(map[string]string),
	}

	var store bigStore
	if opts.Backend == Bolt {
		opts.Durability = High
		store, err = raftboltdb.NewBoltStore(filepath.Join(dir, "raft.db"))
		if err != nil {
			return nil, err
		}
	} else if opts.Backend == LevelDB {
		var dur raftleveldb.Level
		switch opts.Durability {
		default:
			dur = raftleveldb.Medium
			opts.Durability = Medium
		case High:
			dur = raftleveldb.High
		case Low:
			dur = raftleveldb.Low
		}
		store, err = raftleveldb.NewLevelDBStore(filepath.Join(dir, "raft.db"), dur)
		if err != nil {
			return nil, err
		}
	} else if opts.Backend == InMem {
		opts.Durability = Low
		store, err = raftfastlog.NewFastLogStore(":memory:", raftfastlog.Low, n.log.Sub('S'))
		if err != nil {
			return nil, err
		}
	} else {
		opts.Backend = FastLog
		var dur raftfastlog.Level
		switch opts.Durability {
		default:
			dur = raftfastlog.Medium
			opts.Durability = Medium
		case High:
			dur = raftfastlog.High
		case Low:
			dur = raftfastlog.Low
		}
		store, err = raftfastlog.NewFastLogStore(filepath.Join(dir, "raft.db"), dur, n.log.Sub('S'))
		if err != nil {
			return nil, err
		}
	}
	n.store = store

	n.log.Debugf("Consistency: %s, Durability: %s, Backend: %s", opts.Consistency, opts.Durability, opts.Backend)

	// get the peer list
	peers, err := n.store.Peers()
	if err != nil {
		n.Close()
		return nil, err
	}

	// Setup Raft configuration.
	config := raft.DefaultConfig()
	config.LogOutput = n.log

	// Allow the node to enter single-mode, potentially electing itself, if
	// explicitly enabled and there is only 1 node in the cluster already.
	if join == "" && len(peers) <= 1 {
		n.log.Noticef("Enable single node")
		config.EnableSingleNode = true
		config.DisableBootstrapAfterElect = false
	}

	// create the snapshot store. This allows the Raft to truncate the log.
	n.snapshot, err = raft.NewFileSnapshotStore(dir, retainSnapshotCount, n.log)
	if err != nil {
		n.Close()
		return nil, err
	}

	// verify the syntax of the address.
	taddr, err := net.ResolveTCPAddr("tcp", addr)
	if err != nil {
		n.Close()
		return nil, err
	}

	// Set the atomic flag which indicates that we can accept Redcon commands.
	var doReady uint64

	// start the raft server
	n.addr = taddr.String()
	n.trans, err = raftredcon.NewRedconTransport(
		n.addr,
		func(conn redcon.Conn, cmd redcon.Command) {
			if atomic.LoadUint64(&doReady) != 0 {
				n.doCommand(conn, cmd)
			} else {
				conn.WriteError("ERR raft not ready")
			}
		}, opts.ConnAccept, opts.ConnClosed,
		n.log.Sub('L'),
	)
	if err != nil {
		n.Close()
		return nil, err
	}

	// Instantiate the Raft systems.
	n.raft, err = raft.NewRaft(config, (*nodeFSM)(n),
		n.store, n.store, n.snapshot, n.store, n.trans)
	if err != nil {
		n.Close()
		return nil, err
	}

	// set the atomic flag which indicates that we can accept Redcon commands.
	atomic.AddUint64(&doReady, 1)

	// if --join was specified, make the join request.
	for {
		if join != "" && len(peers) == 0 {
			if err := reqRaftJoin(join, n.addr); err != nil {
				if strings.HasPrefix(err.Error(), "TRY ") {
					// we received a "TRY addr" response. let forward the join to
					// the specified address"
					join = strings.Split(err.Error(), " ")[1]
					continue
				}
				return nil, fmt.Errorf("failed to join node at %v: %v", join, err)
			}
		}
		break
	}
	go n.watchPeers()
	return n, nil
}

// Close closes the node
func (n *Node) Close() error {
	n.mu.Lock()
	defer n.mu.Unlock()
	// shutdown the raft, but do not handle the future error. :PPA:
	if n.raft != nil {
		n.raft.Shutdown().Error()
	}
	if n.trans != nil {
		n.trans.Close()
	}
	// close the raft database
	if n.store != nil {
		n.store.Close()
	}
	n.closed = true
	return nil
}

// Store returns the underlying storage object.
func (n *Node) Store() interface{} {
	return n.store
}

func (n *Node) watchPeers() {
	buf := make([]byte, 1024)
	for {
		var peers []string
		var err error
		if !func() bool {
			n.mu.Lock()
			defer n.mu.Unlock()
			if n.closed {
				return false
			}
			peers, err = n.store.Peers()
			return true
		}() {
			return
		}

		func() {
			if err != nil {
				return
			}
			peersState := make(map[string]string)
			for _, peer := range peers {
				state, err := func() (string, error) {
					conn, err := net.DialTimeout("tcp", peer, time.Second)
					if err != nil {
						return "", err
					}
					defer conn.Close()
					if err := conn.SetDeadline(time.Now().Add(time.Second)); err != nil {
						return "", err
					}
					if _, err := conn.Write([]byte("RAFTSTATE\r\n")); err != nil {
						return "", err
					}
					n, err := conn.Read(buf)
					if err != nil {
						return "", err
					}
					parts := strings.Split(string(buf[:n]), "\r\n")
					if len(parts) != 3 || buf[0] != '$' {
						return "", err
					}
					return parts[1], nil
				}()
				if err, ok := err.(net.Error); ok && err.Timeout() {
					state = "Timeout"
				} else if err != nil {
					state = "Invalid"
				}
				peersState[peer] = state
			}
			n.mu.Lock()
			if !n.closed {
				n.peers = peersState
			}
			n.mu.Unlock()
		}()
		time.Sleep(time.Second)
	}
}

// Log returns the active logger for printing messages
func (n *Node) Log() Logger {
	return n.mlog
}

// leader returns the client address for the leader
func (n *Node) leader() string {
	return n.raft.Leader()
}

// reqRaftJoin does a remote "RAFTJOIN" command at the specified address.
func reqRaftJoin(join, raftAddr string) error {
	resp, _, err := raftredcon.Do(join, nil, []byte("raftaddpeer"), []byte(raftAddr))
	if err != nil {
		return err
	}
	if string(resp) != "OK" {
		return errors.New("invalid response")
	}
	return nil
}

// scanForErrors returns pipeline errors. All messages must be errors
func scanForErrors(buf []byte) [][]byte {
	var res [][]byte
	for len(buf) > 0 {
		if buf[0] != '-' {
			return nil
		}
		buf = buf[1:]
		for i := 0; i < len(buf); i++ {
			if buf[i] == '\n' && i > 0 && buf[i-1] == '\r' {
				res = append(res, buf[:i-1])
				buf = buf[i+1:]
				break
			}
		}
	}
	return res
}

func (n *Node) translateError(err error, cmd string) string {
	if err.Error() == ErrDisabled.Error() || err.Error() == ErrUnknownCommand.Error() {
		return "ERR unknown command '" + cmd + "'"
	} else if err.Error() == ErrWrongNumberOfArguments.Error() {
		return "ERR wrong number of arguments for '" + cmd + "' command"
	} else if err.Error() == raft.ErrNotLeader.Error() {
		leader := n.raft.Leader()
		if leader == "" {
			return "ERR leader not known"
		}
		return "TRY " + leader
	}
	return strings.TrimSpace(strings.Split(err.Error(), "\n")[0])
}

// doCommand executes a client command which is processed through the raft pipeline.
func (n *Node) doCommand(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) == 0 {
		return nil, nil
	}
	var val interface{}
	var err error
	switch strings.ToLower(string(cmd.Args[0])) {
	default:
		val, err = n.handler.Command((*nodeApplier)(n), conn, cmd)
		if err == ErrDisabled {
			err = ErrUnknownCommand
		}
	case "raftaddpeer":
		val, err = n.doRaftAddPeer(conn, cmd)
	case "raftremovepeer":
		val, err = n.doRaftRemovePeer(conn, cmd)
	case "raftleader":
		val, err = n.doRaftLeader(conn, cmd)
	case "raftsnapshot":
		val, err = n.doRaftSnapshot(conn, cmd)
	case "raftshrinklog":
		val, err = n.doRaftShrinkLog(conn, cmd)
	case "raftstate":
		val, err = n.doRaftState(conn, cmd)
	case "raftstats":
		val, err = n.doRaftStats(conn, cmd)
	case "raftpeers":
		val, err = n.doRaftPeers(conn, cmd)
	case "quit":
		val, err = n.doQuit(conn, cmd)
	case "ping":
		val, err = n.doPing(conn, cmd)
	}
	if err != nil && conn != nil {
		// it's possible that this was a pipelined response.
		wr := redcon.BaseWriter(conn)
		if wr != nil {
			buf := wr.Buffer()
			rerrs := scanForErrors(buf)
			if len(rerrs) > 0 {
				wr.SetBuffer(nil)
				for _, rerr := range rerrs {
					conn.WriteError(n.translateError(errors.New(string(rerr)), string(cmd.Args[0])))
				}
			}
		}
		conn.WriteError(n.translateError(err, string(cmd.Args[0])))
	}
	return val, err
}

// doPing handles a "PING" client command.
func (n *Node) doPing(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	switch len(cmd.Args) {
	default:
		return nil, ErrWrongNumberOfArguments
	case 1:
		conn.WriteString("PONG")
	case 2:
		conn.WriteBulk(cmd.Args[1])
	}
	return nil, nil
}

// doRaftLeader handles a "RAFTLEADER" client command.
func (n *Node) doRaftLeader(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	leader := n.raft.Leader()
	if leader == "" {
		conn.WriteNull()
	} else {
		conn.WriteBulkString(leader)
	}
	return nil, nil
}

// doRaftSnapshot handles a "RAFTSNAPSHOT" client command.
func (n *Node) doRaftSnapshot(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	f := n.raft.Snapshot()
	err := f.Error()
	if err != nil {
		conn.WriteError("ERR " + err.Error())
		return nil, nil
	}
	conn.WriteString("OK")
	return nil, nil
}

type shrinkable interface {
	Shrink() error
}

// doRaftShrinkLog handles a "RAFTSHRINKLOG" client command.
func (n *Node) doRaftShrinkLog(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	if s, ok := n.store.(shrinkable); ok {
		err := s.Shrink()
		if err != nil {
			conn.WriteError("ERR " + err.Error())
			return nil, nil
		}
		conn.WriteString("OK")
		return nil, nil
	}
	conn.WriteError("ERR log is not shrinkable")
	return nil, nil
}

// doRaftState handles a "RAFTSTATE" client command.
func (n *Node) doRaftState(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	conn.WriteBulkString(n.raft.State().String())
	return nil, nil
}

// doRaftStatus handles a "RAFTSTATUS" client command.
func (n *Node) doRaftStats(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	n.mu.RLock()
	defer n.mu.RUnlock()
	stats := n.raft.Stats()
	keys := make([]string, 0, len(stats))
	for key := range stats {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	conn.WriteArray(len(keys) * 2)
	for _, key := range keys {
		conn.WriteBulkString(key)
		conn.WriteBulkString(stats[key])
	}
	return nil, nil
}

// doRaftStatus handles a "RAFTSTATUS" client command.
func (n *Node) doRaftPeers(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 1 {
		return nil, ErrWrongNumberOfArguments
	}
	var peers []string
	peersState := make(map[string]string)
	func() {
		n.mu.RLock()
		defer n.mu.RUnlock()
		for peer, state := range n.peers {
			peersState[peer] = state
			peers = append(peers, peer)
		}
	}()
	sort.Strings(peers)

	conn.WriteArray(len(peers) * 2)
	for _, peer := range peers {
		conn.WriteBulkString(peer)
		conn.WriteBulkString(peersState[peer])
	}
	return nil, nil
}

// doQuit handles a "QUIT" client command.
func (n *Node) doQuit(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	conn.WriteString("OK")
	conn.Close()
	return nil, nil
}

// doRaftAddPeer handles a "RAFTADDPEER address" client command.
func (n *Node) doRaftAddPeer(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 2 {
		return nil, ErrWrongNumberOfArguments
	}
	n.log.Noticef("Received add peer request from %v", string(cmd.Args[1]))
	f := n.raft.AddPeer(string(cmd.Args[1]))
	if f.Error() != nil {
		return nil, f.Error()
	}
	n.log.Noticef("Node %v added successfully", string(cmd.Args[1]))
	conn.WriteString("OK")
	return nil, nil
}

// doRaftRemovePeer handles a "RAFTREMOVEPEER address" client command.
func (n *Node) doRaftRemovePeer(conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	if len(cmd.Args) != 2 {
		return nil, ErrWrongNumberOfArguments
	}
	n.log.Noticef("Received remove peer request from %v", string(cmd.Args[1]))
	f := n.raft.RemovePeer(string(cmd.Args[1]))
	if f.Error() != nil {
		return nil, f.Error()
	}
	n.log.Noticef("Node %v detached successfully", string(cmd.Args[1]))
	conn.WriteString("OK")
	return nil, nil
}

// raftApplyCommand encodes a series of args into a raft command and
// applies it to the index.
func (n *Node) raftApplyCommand(cmd redcon.Command) (interface{}, error) {
	f := n.raft.Apply(cmd.Raw, raftTimeout)
	if err := f.Error(); err != nil {
		return nil, err
	}
	// we check for the response to be an error and return it as such.
	switch v := f.Response().(type) {
	default:
		return v, nil
	case error:
		return nil, v
	}
}

// raftLevelGuard is used to process readonly commands depending on the
// consistency readonly level.
// It either:
// - low consistency: just processes the command without concern about
//   leadership or cluster state.
// - medium consistency: makes sure that the node is the leader first.
// - high consistency: sends a blank command through the raft pipeline to
// ensure that the node is thel leader, the raft index is incremented, and
// that the cluster is sane before processing the readonly command.
func (n *Node) raftLevelGuard() error {
	switch n.level {
	default:
		// a valid level is required
		return errInvalidConsistencyLevel
	case Low:
		// anything goes.
		return nil
	case Medium:
		// must be the leader
		if n.raft.State() != raft.Leader {
			return raft.ErrNotLeader
		}
		return nil
	case High:
		// process a blank command. this will update the raft log index
		// and allow for readonly commands to process in order without
		// serializing the actual command.
		f := n.raft.Apply(nil, raftTimeout)
		if err := f.Error(); err != nil {
			return err
		}
		// the blank command succeeded.
		v := f.Response()
		// check if response was an error and return that.
		switch v := v.(type) {
		case nil:
			return nil
		case error:
			return v
		}
		return errInvalidResponse
	}
}

// nodeApplier exposes the Applier interface of the Node type
type nodeApplier Node

// Apply executes a command through raft.
// The mutate param should be set to nil for readonly commands.
// The repsond param is required and any response to conn happens here.
// The return value from mutate will be passed into the respond param.
func (m *nodeApplier) Apply(
	conn redcon.Conn,
	cmd redcon.Command,
	mutate func() (interface{}, error),
	respond func(interface{}) (interface{}, error),
) (interface{}, error) {
	var val interface{}
	var err error
	if mutate == nil {
		// no apply, just do a level guard.
		if err := (*Node)(m).raftLevelGuard(); err != nil {
			return nil, err
		}
	} else if conn == nil {
		// this is happening on a follower node.
		return mutate()
	} else {
		// this is happening on the leader node.
		// apply the command to the raft log.
		val, err = (*Node)(m).raftApplyCommand(cmd)
	}
	if err != nil {
		return nil, err
	}
	// responde
	return respond(val)
}

// Log returns the active logger for printing messages
func (m *nodeApplier) Log() Logger {
	return (*Node)(m).Log()
}

// nodeFSM exposes the raft.FSM interface of the Node type
type nodeFSM Node

// Apply applies a Raft log entry to the key-value store.
func (m *nodeFSM) Apply(l *raft.Log) interface{} {
	if len(l.Data) == 0 {
		// blank data
		return nil
	}
	cmd, err := redcon.Parse(l.Data)
	if err != nil {
		return err
	}
	val, err := (*Node)(m).doCommand(nil, cmd)
	if err != nil {
		return err
	}
	return val
}

// Restore stores the key-value store to a previous state.
func (m *nodeFSM) Restore(rc io.ReadCloser) error {
	defer rc.Close()
	return (*Node)(m).handler.Restore(rc)
}

// Persist writes the snapshot to the given sink.
func (m *nodeFSM) Persist(sink raft.SnapshotSink) error {
	if err := (*Node)(m).handler.Snapshot(sink); err != nil {
		sink.Cancel()
		return err
	}
	sink.Close()
	return nil
}

// Release deletes the temp file
func (m *nodeFSM) Release() {}

// Snapshot returns a snapshot of the key-value store.
func (m *nodeFSM) Snapshot() (raft.FSMSnapshot, error) {
	return m, nil
}


================================================
FILE: finn_test.go
================================================
package finn

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"os"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/tidwall/raft-redcon"
	"github.com/tidwall/redcon"
)

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

func NewKVM() *KVM {
	return &KVM{
		keys: make(map[string][]byte),
	}
}
func (kvm *KVM) Command(m Applier, conn redcon.Conn, cmd redcon.Command) (interface{}, error) {
	switch strings.ToLower(string(cmd.Args[0])) {
	default:
		return nil, ErrUnknownCommand
	case "set":
		if len(cmd.Args) != 3 {
			return nil, ErrWrongNumberOfArguments
		}
		return m.Apply(conn, cmd,
			func() (interface{}, error) {
				kvm.mu.Lock()
				defer kvm.mu.Unlock()
				kvm.keys[string(cmd.Args[1])] = cmd.Args[2]
				return nil, nil
			},
			func(v interface{}) (interface{}, error) {
				conn.WriteString("OK")
				return nil, nil
			},
		)
	case "get":
		if len(cmd.Args) != 2 {
			return nil, ErrWrongNumberOfArguments
		}
		return m.Apply(conn, cmd,
			nil,
			func(interface{}) (interface{}, error) {
				kvm.mu.RLock()
				defer kvm.mu.RUnlock()
				if val, ok := kvm.keys[string(cmd.Args[1])]; !ok {
					conn.WriteNull()
				} else {
					conn.WriteBulk(val)
				}
				return nil, nil
			},
		)
	}
}

func (kvm *KVM) 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 *KVM) 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
}

var killed = make(map[int]bool)
var killCond = sync.NewCond(&sync.Mutex{})

func killNodes(basePort int) {
	killCond.L.Lock()
	killed[basePort] = true
	killCond.Broadcast()
	killCond.L.Unlock()
}

func startTestNode(t testing.TB, basePort int, num int, opts *Options) {
	node := fmt.Sprintf("%d", num)

	if err := os.MkdirAll("data/"+node, 0700); err != nil {
		t.Fatal(err)
	}
	join := ""
	if node == "" {
		node = "0"
	}
	addr := fmt.Sprintf(":%d", basePort/10*10+num)
	if node != "0" {
		join = fmt.Sprintf(":%d", basePort)
	}
	n, err := Open("data/"+node, addr, join, NewKVM(), opts)
	if err != nil {
		t.Fatal(err)
	}
	defer n.Close()
	for {
		killCond.L.Lock()
		if killed[basePort] {
			killCond.L.Unlock()
			return
		}
		killCond.Wait()
		killCond.L.Unlock()
	}
}

func waitFor(t testing.TB, basePort, node int) {
	target := fmt.Sprintf(":%d", basePort/10*10+node)
	start := time.Now()
	for {
		if time.Now().Sub(start) > time.Second*10 {
			t.Fatal("timeout looking for leader")
		}
		time.Sleep(time.Second / 4)
		resp, _, err := raftredcon.Do(target, nil, []byte("raftleader"))
		if err != nil {
			continue
		}
		if len(resp) != 0 {
			return
		}
	}
}

func testDo(t testing.TB, basePort, node int, expect string, args ...string) string {
	var bargs [][]byte
	for _, arg := range args {
		bargs = append(bargs, []byte(arg))
	}
	target := fmt.Sprintf(":%d", basePort/10*10+node)
	resp, _, err := raftredcon.Do(target, nil, bargs...)
	if err != nil {
		if err.Error() == expect {
			return ""
		}
		t.Fatalf("node %d: %v", node, err)
	}
	if expect != "???" && string(resp) != expect {
		t.Fatalf("node %d: expected '%v', got '%v'", node, expect, string(resp))
	}
	return string(resp)
}

func TestVarious(t *testing.T) {
	t.Run("Level", SubTestLevel)
	t.Run("Backend", SubTestBackend)
}

func SubTestLevel(t *testing.T) {
	var level Level
	level = Level(-99)
	if level.String() != "unknown" {
		t.Fatalf("expecting '%v', got '%v'", "unknown", level.String())
	}
	level = Low
	if level.String() != "low" {
		t.Fatalf("expecting '%v', got '%v'", "low", level.String())
	}
	level = Medium
	if level.String() != "medium" {
		t.Fatalf("expecting '%v', got '%v'", "medium", level.String())
	}
	level = High
	if level.String() != "high" {
		t.Fatalf("expecting '%v', got '%v'", "high", level.String())
	}
}

func SubTestBackend(t *testing.T) {
	var backend Backend
	backend = Backend(-99)
	if backend.String() != "unknown" {
		t.Fatalf("expecting '%v', got '%v'", "unknown", backend.String())
	}
	backend = FastLog
	if backend.String() != "fastlog" {
		t.Fatalf("expecting '%v', got '%v'", "fastlog", backend.String())
	}
	backend = Bolt
	if backend.String() != "bolt" {
		t.Fatalf("expecting '%v', got '%v'", "bolt", backend.String())
	}
	backend = LevelDB
	if backend.String() != "leveldb" {
		t.Fatalf("expecting '%v', got '%v'", "leveldb", backend.String())
	}
	backend = InMem
	if backend.String() != "inmem" {
		t.Fatalf("expecting '%v', got '%v'", "inmem", backend.String())
	}
}

func TestCluster(t *testing.T) {

	var optsArr []Options
	for _, backend := range []Backend{LevelDB, Bolt, FastLog, InMem} {
		for _, consistency := range []Level{Low, Medium, High} {
			optsArr = append(optsArr, Options{
				Backend:     backend,
				Consistency: consistency,
			})
		}
	}
	for i := 0; i < len(optsArr); i++ {
		func() {
			opts := optsArr[i]
			if os.Getenv("LOG") != "1" {
				opts.LogOutput = ioutil.Discard
			}
			basePort := (7480/10 + i) * 10
			tag := fmt.Sprintf("%v-%v-%d", opts.Backend, opts.Consistency, basePort)
			t.Logf("%s", tag)
			t.Run(tag, func(t *testing.T) {
				os.RemoveAll("data")
				defer os.RemoveAll("data")
				defer killNodes(basePort)
				for i := 0; i < 3; i++ {
					go startTestNode(t, basePort, i, &opts)
					waitFor(t, basePort, i)
				}
				t.Run("Leader", func(t *testing.T) { SubTestLeader(t, basePort, &opts) })
				t.Run("Set", func(t *testing.T) { SubTestSet(t, basePort, &opts) })
				t.Run("Get", func(t *testing.T) { SubTestGet(t, basePort, &opts) })
				t.Run("Snapshot", func(t *testing.T) { SubTestSnapshot(t, basePort, &opts) })
				t.Run("Ping", func(t *testing.T) { SubTestPing(t, basePort, &opts) })
				t.Run("RaftShrinkLog", func(t *testing.T) { SubTestRaftShrinkLog(t, basePort, &opts) })
				t.Run("RaftStats", func(t *testing.T) { SubTestRaftStats(t, basePort, &opts) })
				t.Run("RaftState", func(t *testing.T) { SubTestRaftState(t, basePort, &opts) })
				t.Run("AddPeer", func(t *testing.T) { SubTestAddPeer(t, basePort, &opts) })
				t.Run("RemovePeer", func(t *testing.T) { SubTestRemovePeer(t, basePort, &opts) })
			})
		}()
	}
}

func SubTestLeader(t *testing.T, basePort int, opts *Options) {
	baseAddr := fmt.Sprintf(":%d", basePort)
	testDo(t, basePort, 0, baseAddr, "raftleader")
	testDo(t, basePort, 1, baseAddr, "raftleader")
	testDo(t, basePort, 2, baseAddr, "raftleader")
}

func SubTestSet(t *testing.T, basePort int, opts *Options) {
	baseAddr := fmt.Sprintf(":%d", basePort)
	testDo(t, basePort, 0, "OK", "set", "hello", "world")
	testDo(t, basePort, 1, "TRY "+baseAddr, "set", "hello", "world")
	testDo(t, basePort, 2, "TRY "+baseAddr, "set", "hello", "world")
}

func SubTestGet(t *testing.T, basePort int, opts *Options) {
	baseAddr := fmt.Sprintf(":%d", basePort)
	testDo(t, basePort, 0, "world", "get", "hello")
	testDo(t, basePort, 1, "TRY "+baseAddr, "set", "hello", "world")
	testDo(t, basePort, 2, "TRY "+baseAddr, "set", "hello", "world")
}

func SubTestPing(t *testing.T, basePort int, opts *Options) {
	for i := 0; i < 3; i++ {
		testDo(t, basePort, i, "PONG", "ping")
		testDo(t, basePort, i, "HELLO", "ping", "HELLO")
		testDo(t, basePort, i, "ERR wrong number of arguments for 'ping' command", "ping", "HELLO", "WORLD")
	}
}

func SubTestRaftShrinkLog(t *testing.T, basePort int, opts *Options) {
	for i := 0; i < 3; i++ {
		if opts.Backend == Bolt || opts.Backend == LevelDB {
			testDo(t, basePort, i, "ERR log is not shrinkable", "raftshrinklog")
		} else {
			testDo(t, basePort, i, "OK", "raftshrinklog")
		}
		testDo(t, basePort, i, "ERR wrong number of arguments for 'raftshrinklog' command", "raftshrinklog", "abc")
	}
}
func SubTestRaftStats(t *testing.T, basePort int, opts *Options) {
	for i := 0; i < 3; i++ {
		resp := testDo(t, basePort, i, "???", "raftstats")
		if !strings.Contains(resp, "applied_index") || !strings.Contains(resp, "num_peers") {
			t.Fatal("expected values")
		}
		testDo(t, basePort, i, "ERR wrong number of arguments for 'raftstats' command", "raftstats", "abc")
	}
}
func SubTestRaftState(t *testing.T, basePort int, opts *Options) {
	for i := 0; i < 3; i++ {
		if i == 0 {
			testDo(t, basePort, i, "Leader", "raftstate")
		} else {
			testDo(t, basePort, i, "Follower", "raftstate")
		}
		testDo(t, basePort, i, "ERR wrong number of arguments for 'raftstate' command", "raftstate", "abc")
	}
}
func SubTestSnapshot(t *testing.T, basePort int, opts *Options) {
	// insert 1000 items
	for i := 0; i < 1000; i++ {
		testDo(t, basePort, 0, "OK", "set", fmt.Sprintf("key:%d", i), fmt.Sprintf("val:%d", i))
	}
	testDo(t, basePort, 0, "OK", "raftsnapshot")
	testDo(t, basePort, 1, "OK", "raftsnapshot")
	testDo(t, basePort, 2, "OK", "raftsnapshot")
}
func SubTestAddPeer(t *testing.T, basePort int, opts *Options) {
	baseAddr := fmt.Sprintf(":%d", basePort)
	go startTestNode(t, basePort, 3, opts)
	waitFor(t, basePort, 3)
	testDo(t, basePort, 3, baseAddr, "raftleader")
	testDo(t, basePort, 3, "TRY "+baseAddr, "set", "hello", "world")
	testDo(t, basePort, 3, "OK", "raftsnapshot")
}

func SubTestRemovePeer(t *testing.T, basePort int, opts *Options) {
	baseAddr := fmt.Sprintf(":%d", basePort)
	testDo(t, basePort, 1, "TRY "+baseAddr, "raftremovepeer", fmt.Sprintf(":%d3", basePort/10))
	testDo(t, basePort, 0, "OK", "raftremovepeer", fmt.Sprintf(":%d3", basePort/10))
	testDo(t, basePort, 0, "peer is unknown", "raftremovepeer", fmt.Sprintf(":%d3", basePort/10))
}

func BenchmarkCluster(t *testing.B) {
	os.RemoveAll("data")
	defer os.RemoveAll("data")
	for i := 0; i < 3; i++ {
		go startTestNode(t, 7480, i, &Options{LogOutput: ioutil.Discard})
		waitFor(t, 7480, i)
	}
	t.Run("PL", func(t *testing.B) {
		pl := []int{1, 4, 16, 64}
		for i := 0; i < len(pl); i++ {
			func(pl int) {
				t.Run(fmt.Sprintf("%d", pl), func(t *testing.B) {
					t.Run("Ping", func(t *testing.B) { SubBenchmarkPing(t, pl) })
					t.Run("Set", func(t *testing.B) { SubBenchmarkSet(t, pl) })
					t.Run("Get", func(t *testing.B) { SubBenchmarkGet(t, pl) })
				})
			}(pl[i])
		}
	})
}
func testDial(t testing.TB, node int) (net.Conn, *bufio.ReadWriter) {
	conn, err := net.Dial("tcp", fmt.Sprintf(":748%d", node))
	if err != nil {
		t.Fatal(err)
	}
	return conn, bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
}
func buildCommand(args ...string) []byte {
	var buf []byte
	buf = append(buf, '*')
	buf = append(buf, strconv.FormatInt(int64(len(args)), 10)...)
	buf = append(buf, '\r', '\n')
	for _, arg := range args {
		buf = append(buf, '$')
		buf = append(buf, strconv.FormatInt(int64(len(arg)), 10)...)
		buf = append(buf, '\r', '\n')
		buf = append(buf, arg...)
		buf = append(buf, '\r', '\n')
	}
	return buf
}

func testConnDo(t testing.TB, rw *bufio.ReadWriter, pl int, expect string, cmd []byte) {
	for i := 0; i < pl; i++ {
		rw.Write(cmd)
	}
	if err := rw.Flush(); err != nil {
		t.Fatal(err)
	}
	buf := make([]byte, len(expect))
	for i := 0; i < pl; i++ {
		if _, err := io.ReadFull(rw, buf); err != nil {
			t.Fatal(err)
		}
		if string(buf) != expect {
			t.Fatalf("expected '%v', got '%v'", expect, string(buf))
		}
	}
}

func SubBenchmarkPing(t *testing.B, pipeline int) {
	conn, rw := testDial(t, 0)
	defer conn.Close()
	t.ResetTimer()
	for i := 0; i < t.N; i += pipeline {
		n := pipeline
		if t.N-i < pipeline {
			n = t.N - i
		}
		testConnDo(t, rw, n, "+PONG\r\n", []byte("*1\r\n$4\r\nPING\r\n"))
	}
}

func SubBenchmarkSet(t *testing.B, pipeline int) {
	conn, rw := testDial(t, 0)
	defer conn.Close()
	t.ResetTimer()
	for i := 0; i < t.N; i += pipeline {
		n := pipeline
		if t.N-i < pipeline {
			n = t.N - i
		}
		testConnDo(t, rw, n, "+OK\r\n", buildCommand("set", fmt.Sprintf("key:%d", i), fmt.Sprintf("val:%d", i)))
	}
}

func SubBenchmarkGet(t *testing.B, pipeline int) {
	conn, rw := testDial(t, 0)
	defer conn.Close()
	t.ResetTimer()
	for i := 0; i < t.N; i += pipeline {
		n := pipeline
		if t.N-i < pipeline {
			n = t.N - i
		}
		testConnDo(t, rw, n, "$-1\r\n", buildCommand("get", "key:na"))
	}
}


================================================
FILE: go.mod
================================================
module github.com/tidwall/finn

go 1.13

require (
	github.com/armon/go-metrics v0.3.0 // indirect
	github.com/boltdb/bolt v1.3.1 // indirect
	github.com/garyburd/redigo v1.6.0 // indirect
	github.com/hashicorp/go-msgpack v0.5.5 // indirect
	github.com/hashicorp/raft v0.1.0
	github.com/syndtr/goleveldb v1.0.0 // indirect
	github.com/tidwall/match v1.0.1 // indirect
	github.com/tidwall/raft-boltdb v0.0.0-20160909211738-25b87f2c5677
	github.com/tidwall/raft-fastlog v0.0.0-20160922202426-2f0d0a0ce558
	github.com/tidwall/raft-leveldb v0.0.0-20170127185243-ada471496dc9
	github.com/tidwall/raft-redcon v0.1.0
	github.com/tidwall/redcon v1.0.0
	github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893
	golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
)


================================================
FILE: go.sum
================================================
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU=
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.1 h1:9PZfAcVEvez4yhLH2TBU64/h/z4xlFI80cWXRrxuKuM=
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/raft v0.0.0-20160824023112-5f09c4ffdbcd h1:gN6xm3iAclW5DKJWYiXO8tZN25Zy7UsB6Wh/85OB8Bg=
github.com/hashicorp/raft v0.0.0-20160824023112-5f09c4ffdbcd/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI=
github.com/hashicorp/raft v0.1.0 h1:OC+j7LWkv7x8s9c5wnXCEgtP1J0LDw2fKNxUiYCZFNo=
github.com/hashicorp/raft v0.1.0/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI=
github.com/hashicorp/raft v1.1.1 h1:HJr7UE1x/JrJSc9Oy6aDBHtNHUUBHjcQjTgvUVihoZs=
github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/raft v1.0.0 h1:XuXkumePQBVcBYtfC57f7dXRM4WBZei3lZkK06+0D8I=
github.com/tidwall/raft v1.0.0/go.mod h1:uMALL7ToL5LoHWCGwLE1uROh9W0ormbwrdxg2uSo7Oo=
github.com/tidwall/raft-boltdb v0.0.0-20160909211738-25b87f2c5677 h1:8FkXr+GCV4wb8WAct/V1vKB/Ivy11Y+fm919EHgdfWA=
github.com/tidwall/raft-boltdb v0.0.0-20160909211738-25b87f2c5677/go.mod h1:O7b2tvwZmC+IFu8djLOZj0jc/tjssDPiJ8xIt+U2jTU=
github.com/tidwall/raft-boltdb v0.0.0-20180905173017-ae4e25b230d8 h1:D9uqhiFILz+qx8y2LtX70pDvjCYXEghnUTZ934M+fuY=
github.com/tidwall/raft-boltdb v0.0.0-20180905173017-ae4e25b230d8/go.mod h1:O7b2tvwZmC+IFu8djLOZj0jc/tjssDPiJ8xIt+U2jTU=
github.com/tidwall/raft-fastlog v0.0.0-20160922202426-2f0d0a0ce558 h1:hQYEIfMzrH6LRzjz7Jp5Rv8jrty1bAR5M0DjOYSxxks=
github.com/tidwall/raft-fastlog v0.0.0-20160922202426-2f0d0a0ce558/go.mod h1:KNwBhka/a5Ucw5bfEzKHTEKuCO2Do1tKs+kDdu3Sbb4=
github.com/tidwall/raft-fastlog v0.0.0-20190329194628-f798a12ed2b3 h1:Km24Wbatpk4a0cQlmW1lGvyjzDD2biQlaqtqR1G7Cic=
github.com/tidwall/raft-fastlog v0.0.0-20190329194628-f798a12ed2b3/go.mod h1:KNwBhka/a5Ucw5bfEzKHTEKuCO2Do1tKs+kDdu3Sbb4=
github.com/tidwall/raft-leveldb v0.0.0-20170127185243-ada471496dc9 h1:Z5QMqF/MSuvnrTibHqs/xx+ZE5gypLV02YU8Ry4kJ7A=
github.com/tidwall/raft-leveldb v0.0.0-20170127185243-ada471496dc9/go.mod h1:KNAMyK8s/oUOTbIL/T07fTL6/EgJfHhK8XeeEPq35eU=
github.com/tidwall/raft-leveldb v0.0.0-20180905172604-d81b19dd795a h1:wSOV25XXv0kdoWUEqCYEgaPAgWm5mdi3c1wkisYdQaM=
github.com/tidwall/raft-leveldb v0.0.0-20180905172604-d81b19dd795a/go.mod h1:KNAMyK8s/oUOTbIL/T07fTL6/EgJfHhK8XeeEPq35eU=
github.com/tidwall/raft-leveldb v0.0.0-20190319171839-8607dc18110d h1:DypM2TD6Pdev1QH5WrwLO09jB1oq7KvWpYvnqCS3Vow=
github.com/tidwall/raft-leveldb v0.0.0-20190319171839-8607dc18110d/go.mod h1:KNAMyK8s/oUOTbIL/T07fTL6/EgJfHhK8XeeEPq35eU=
github.com/tidwall/raft-redcon v0.1.0 h1:qwYaFaAVNFleY2EFm0j7UK4vEpoNa19ohH7U4idbg+s=
github.com/tidwall/raft-redcon v0.1.0/go.mod h1:YhoECfJs8MXbrwak9H7wKYDMZ3rMaB7el7zZ7MRw9Xw=
github.com/tidwall/redcon v1.0.0 h1:D4AzzJ81Afeh144fgnj5H0aSVPBBJ5RI9Rzj0zThU+E=
github.com/tidwall/redcon v1.0.0/go.mod h1:bdYBm4rlcWpst2XMwKVzWDF9CoUxEbUmM7CQrKeOZas=
github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893 h1:aGyVYs0o1pThR9i+SuYCG/VqWibHkUXl9kIMZGhAXDw=
github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893/go.mod h1:NssoNA+Uwqd5WHKkVwAzO7AT6VuG3wiC8r5nBqds3Ao=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5 h1:sM3evRHxE/1RuMe1FYAL3j7C7fUfIjkbE+NiDAYUF8U=
golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Download .txt
gitextract_cfjio72r/

├── LICENSE
├── README.md
├── example/
│   └── clone.go
├── finn.go
├── finn_test.go
├── go.mod
└── go.sum
Download .txt
SYMBOL INDEX (93 symbols across 3 files)

FILE: example/clone.go
  function main (line 19) | func main() {
  type Clone (line 94) | type Clone struct
    method Command (line 107) | func (kvm *Clone) Command(m finn.Applier, conn redcon.Conn, cmd redcon...
    method Restore (line 195) | func (kvm *Clone) Restore(rd io.Reader) error {
    method Snapshot (line 211) | func (kvm *Clone) Snapshot(wr io.Writer) error {
  function NewClone (line 100) | func NewClone() *Clone {

FILE: finn.go
  constant retainSnapshotCount (line 43) | retainSnapshotCount = 2
  constant raftTimeout (line 44) | raftTimeout         = 10 * time.Second
  type Level (line 48) | type Level
    method String (line 51) | func (l Level) String() string {
  constant Low (line 67) | Low Level = -1
  constant Medium (line 73) | Medium Level = 0
  constant High (line 76) | High Level = 1
  type Backend (line 80) | type Backend
    method String (line 95) | func (b Backend) String() string {
  constant FastLog (line 85) | FastLog Backend = iota
  constant Bolt (line 87) | Bolt
  constant InMem (line 89) | InMem
  constant LevelDB (line 91) | LevelDB
  type LogLevel (line 111) | type LogLevel
  constant Debug (line 115) | Debug LogLevel = -2
  constant Verbose (line 117) | Verbose LogLevel = -1
  constant Notice (line 119) | Notice LogLevel = 0
  constant Warning (line 121) | Warning LogLevel = 1
  type Options (line 125) | type Options struct
  function fillOptions (line 154) | func fillOptions(opts *Options) *Options {
  type Logger (line 167) | type Logger interface
  type Applier (line 181) | type Applier interface
  type Machine (line 191) | type Machine interface
  type Node (line 202) | type Node struct
    method Close (line 409) | func (n *Node) Close() error {
    method Store (line 428) | func (n *Node) Store() interface{} {
    method watchPeers (line 432) | func (n *Node) watchPeers() {
    method Log (line 495) | func (n *Node) Log() Logger {
    method leader (line 500) | func (n *Node) leader() string {
    method translateError (line 535) | func (n *Node) translateError(err error, cmd string) string {
    method doCommand (line 551) | func (n *Node) doCommand(conn redcon.Conn, cmd redcon.Command) (interf...
    method doPing (line 603) | func (n *Node) doPing(conn redcon.Conn, cmd redcon.Command) (interface...
    method doRaftLeader (line 616) | func (n *Node) doRaftLeader(conn redcon.Conn, cmd redcon.Command) (int...
    method doRaftSnapshot (line 630) | func (n *Node) doRaftSnapshot(conn redcon.Conn, cmd redcon.Command) (i...
    method doRaftShrinkLog (line 649) | func (n *Node) doRaftShrinkLog(conn redcon.Conn, cmd redcon.Command) (...
    method doRaftState (line 667) | func (n *Node) doRaftState(conn redcon.Conn, cmd redcon.Command) (inte...
    method doRaftStats (line 676) | func (n *Node) doRaftStats(conn redcon.Conn, cmd redcon.Command) (inte...
    method doRaftPeers (line 697) | func (n *Node) doRaftPeers(conn redcon.Conn, cmd redcon.Command) (inte...
    method doQuit (line 722) | func (n *Node) doQuit(conn redcon.Conn, cmd redcon.Command) (interface...
    method doRaftAddPeer (line 729) | func (n *Node) doRaftAddPeer(conn redcon.Conn, cmd redcon.Command) (in...
    method doRaftRemovePeer (line 744) | func (n *Node) doRaftRemovePeer(conn redcon.Conn, cmd redcon.Command) ...
    method raftApplyCommand (line 760) | func (n *Node) raftApplyCommand(cmd redcon.Command) (interface{}, erro...
    method raftLevelGuard (line 783) | func (n *Node) raftLevelGuard() error {
  type bigStore (line 220) | type bigStore interface
  function Open (line 237) | func Open(dir, addr, join string, handler Machine, opts *Options) (node ...
  function reqRaftJoin (line 505) | func reqRaftJoin(join, raftAddr string) error {
  function scanForErrors (line 517) | func scanForErrors(buf []byte) [][]byte {
  type shrinkable (line 644) | type shrinkable interface
  type nodeApplier (line 819) | type nodeApplier
    method Apply (line 825) | func (m *nodeApplier) Apply(
    method Log (line 854) | func (m *nodeApplier) Log() Logger {
  type nodeFSM (line 859) | type nodeFSM
    method Apply (line 862) | func (m *nodeFSM) Apply(l *raft.Log) interface{} {
    method Restore (line 879) | func (m *nodeFSM) Restore(rc io.ReadCloser) error {
    method Persist (line 885) | func (m *nodeFSM) Persist(sink raft.SnapshotSink) error {
    method Release (line 895) | func (m *nodeFSM) Release() {}
    method Snapshot (line 898) | func (m *nodeFSM) Snapshot() (raft.FSMSnapshot, error) {

FILE: finn_test.go
  type KVM (line 21) | type KVM struct
    method Command (line 31) | func (kvm *KVM) Command(m Applier, conn redcon.Conn, cmd redcon.Comman...
    method Restore (line 71) | func (kvm *KVM) Restore(rd io.Reader) error {
    method Snapshot (line 86) | func (kvm *KVM) Snapshot(wr io.Writer) error {
  function NewKVM (line 26) | func NewKVM() *KVM {
  function killNodes (line 102) | func killNodes(basePort int) {
  function startTestNode (line 109) | func startTestNode(t testing.TB, basePort int, num int, opts *Options) {
  function waitFor (line 139) | func waitFor(t testing.TB, basePort, node int) {
  function testDo (line 157) | func testDo(t testing.TB, basePort, node int, expect string, args ...str...
  function TestVarious (line 176) | func TestVarious(t *testing.T) {
  function SubTestLevel (line 181) | func SubTestLevel(t *testing.T) {
  function SubTestBackend (line 201) | func SubTestBackend(t *testing.T) {
  function TestCluster (line 225) | func TestCluster(t *testing.T) {
  function SubTestLeader (line 268) | func SubTestLeader(t *testing.T, basePort int, opts *Options) {
  function SubTestSet (line 275) | func SubTestSet(t *testing.T, basePort int, opts *Options) {
  function SubTestGet (line 282) | func SubTestGet(t *testing.T, basePort int, opts *Options) {
  function SubTestPing (line 289) | func SubTestPing(t *testing.T, basePort int, opts *Options) {
  function SubTestRaftShrinkLog (line 297) | func SubTestRaftShrinkLog(t *testing.T, basePort int, opts *Options) {
  function SubTestRaftStats (line 307) | func SubTestRaftStats(t *testing.T, basePort int, opts *Options) {
  function SubTestRaftState (line 316) | func SubTestRaftState(t *testing.T, basePort int, opts *Options) {
  function SubTestSnapshot (line 326) | func SubTestSnapshot(t *testing.T, basePort int, opts *Options) {
  function SubTestAddPeer (line 335) | func SubTestAddPeer(t *testing.T, basePort int, opts *Options) {
  function SubTestRemovePeer (line 344) | func SubTestRemovePeer(t *testing.T, basePort int, opts *Options) {
  function BenchmarkCluster (line 351) | func BenchmarkCluster(t *testing.B) {
  function testDial (line 371) | func testDial(t testing.TB, node int) (net.Conn, *bufio.ReadWriter) {
  function buildCommand (line 378) | func buildCommand(args ...string) []byte {
  function testConnDo (line 393) | func testConnDo(t testing.TB, rw *bufio.ReadWriter, pl int, expect strin...
  function SubBenchmarkPing (line 411) | func SubBenchmarkPing(t *testing.B, pipeline int) {
  function SubBenchmarkSet (line 424) | func SubBenchmarkSet(t *testing.B, pipeline int) {
  function SubBenchmarkGet (line 437) | func SubBenchmarkGet(t *testing.B, pipeline int) {
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (68K chars).
[
  {
    "path": "LICENSE",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Josh Baker\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "README.md",
    "chars": 10658,
    "preview": "**This project has been archived. Please check out [Uhaha](https://github.com/tidwall/uhaha) for a fitter, happier, more"
  },
  {
    "path": "example/clone.go",
    "chars": 4938,
    "preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.co"
  },
  {
    "path": "finn.go",
    "chars": 23183,
    "preview": "// Package finn provide a fast and simple Raft implementation.\npackage finn\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os"
  },
  {
    "path": "finn_test.go",
    "chars": 12236,
    "preview": "package finn\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\""
  },
  {
    "path": "go.mod",
    "chars": 776,
    "preview": "module github.com/tidwall/finn\n\ngo 1.13\n\nrequire (\n\tgithub.com/armon/go-metrics v0.3.0 // indirect\n\tgithub.com/boltdb/bo"
  },
  {
    "path": "go.sum",
    "chars": 9810,
    "preview": "github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/Data"
  }
]

About this extraction

This page contains the full source code of the tidwall/finn GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (61.2 KB), approximately 20.7k tokens, and a symbol index with 93 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!