Repository: goraft/raftd Branch: master Commit: 404acbea0f48 Files: 7 Total size: 13.0 KB Directory structure: gitextract_qomad0nw/ ├── .gitignore ├── LICENSE ├── README.md ├── command/ │ └── write_command.go ├── db/ │ └── db.go ├── main.go └── server/ └── server.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe ================================================ FILE: LICENSE ================================================ Copyright 2013 raftd contributors 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 ================================================ raftd ===== ![unmaintained](http://img.shields.io/badge/status-unmaintained-red.png) **NOTE**: This project is unmaintained. If you are using goraft in a project and want to carry the project forward please file an issue with your ideas and intentions. The original project authors have created new raft implementations now used in [etcd](https://godoc.org/github.com/coreos/etcd/raft) and [InfluxDB](https://godoc.org/github.com/influxdb/influxdb/raft). If you want to see a simple raft key-value store see the [etcd/raft example](https://github.com/coreos/etcd/tree/master/contrib/raftexample). ## Overview The raftd server is a reference implementation for using the [goraft](https://github.com/goraft/raft) library. This library provides a distributed consensus protocol based on the Raft protocol as described by Diego Ongaro and John Ousterhout in their paper, [In Search of an Understandable Consensus Algorithm](https://ramcloud.stanford.edu/wiki/download/attachments/11370504/raft.pdf). This protocol is based on Paxos but is architected to be more understandable. It is similar to other log-based distributed consensus systems such as [Google's Chubby](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CDAQFjAA&url=http%3A%2F%2Fresearch.google.com%2Farchive%2Fchubby.html&ei=i9OGUerTJKbtiwLkiICoCQ&usg=AFQjCNEmFWlaB_iXQfEjMcMwPaYTphO6bA&sig2=u1vefM2ZOZu_ZVIZGynt1A&bvm=bv.45960087,d.cGE) or [Heroku's doozerd](https://github.com/ha/doozerd). This reference implementation is very simple. It is a key/value database with the following HTTP API: ``` # Set the value of a key. $ curl -X POST http://localhost:4001/db/my_key -d 'FOO' ``` ``` # Retrieve the value for a given key. $ curl http://localhost:4001/db/my_key FOO ``` All the values sent to the leader will be propagated to the other servers in the cluster. This reference implementation does not support command forwarding. If you try to send a change to a follower then it will simply be denied. ## Running First, install raftd: ```sh $ go get github.com/goraft/raftd ``` To start the first node in your cluster, simply specify a port and a directory where the data will be stored: ```sh $ raftd -p 4001 ~/node.1 ``` To add nodes to the cluster, you'll need to start on a different port and use a different data directory. You'll also need to specify the host/port of the leader of the cluster to join: ```sh $ raftd -p 4002 -join localhost:4001 ~/node.2 ``` When you restart the node, it's already been joined to the cluster so you can remove the `-join` argument. Finally, you can add one more node: ```sh $ raftd -p 4003 -join localhost:4001 ~/node.3 ``` Now when you set values to the leader: ```sh $ curl -XPOST localhost:4001/db/foo -d 'bar' ``` The values will be propagated to the followers: ```sh $ curl localhost:4001/db/foo bar $ curl localhost:4002/db/foo bar $ curl localhost:4003/db/foo bar ``` Killing the leader will automatically elect a new leader. If you kill and restart the first node and try to set a value you'll receive: ```sh $ curl -XPOST localhost:4001/db/foo -d 'bar' raft.Server: Not current leader ``` Leader forwarding is not implemented in the reference implementation. ## FAQ ### Why is command forwarding not implemented? Command forwarding is a nice feature to have because it allows a client to send a command to any server and have it pushed to the current leader. This lets your client code stay simple. However, now you have an additional point of failure in your remote call. If the intermediate server crashes while delivering the command then your client will still need to know how to retry its command. Since this retry logic needs to be in your client code, adding command forwarding doesn't provide any benefit. ### Why isn't feature X implemented? Raftd is meant to be a basic reference implementation. As such, it's aim is to provide the smallest, simplest implementation required to get someone off the ground and using go-raft in their project. If you have questions on how to implement a given feature, please add a Github Issue and we can provide instructions in this README. ## Debugging If you want to see more detail then you can specify several options for logging: ``` -v Enables verbose raftd logging. -debug Enables debug-level raft logging. -trace Enables trace-level raft logging. ``` If you're having an issue getting `raftd` running, the `-debug` and `-trace` options can be really useful. ## Caveats One issue with running a 2-node distributed consensus protocol is that we need both servers operational to make a quorum and to perform an actions on the server. So if we kill one of the servers at this point, we will not be able to update the system (since we can't replicate to a majority). You will need to add additional nodes to allow failures to not affect the system. For example, with 3 nodes you can have 1 node fail. With 5 nodes you can have 2 nodes fail. ================================================ FILE: command/write_command.go ================================================ package command import ( "github.com/goraft/raft" "github.com/goraft/raftd/db" ) // This command writes a value to a key. type WriteCommand struct { Key string `json:"key"` Value string `json:"value"` } // Creates a new write command. func NewWriteCommand(key string, value string) *WriteCommand { return &WriteCommand{ Key: key, Value: value, } } // The name of the command in the log. func (c *WriteCommand) CommandName() string { return "write" } // Writes a value to a key. func (c *WriteCommand) Apply(server raft.Server) (interface{}, error) { db := server.Context().(*db.DB) db.Put(c.Key, c.Value) return nil, nil } ================================================ FILE: db/db.go ================================================ package db import ( "sync" ) // The key-value database. type DB struct { data map[string]string mutex sync.RWMutex } // Creates a new database. func New() *DB { return &DB{ data: make(map[string]string), } } // Retrieves the value for a given key. func (db *DB) Get(key string) string { db.mutex.RLock() defer db.mutex.RUnlock() return db.data[key] } // Sets the value for a given key. func (db *DB) Put(key string, value string) { db.mutex.Lock() defer db.mutex.Unlock() db.data[key] = value } ================================================ FILE: main.go ================================================ package main import ( "flag" "fmt" "github.com/goraft/raft" "github.com/goraft/raftd/command" "github.com/goraft/raftd/server" "log" "math/rand" "os" "time" ) var verbose bool var trace bool var debug bool var host string var port int var join string func init() { flag.BoolVar(&verbose, "v", false, "verbose logging") flag.BoolVar(&trace, "trace", false, "Raft trace debugging") flag.BoolVar(&debug, "debug", false, "Raft debugging") flag.StringVar(&host, "h", "localhost", "hostname") flag.IntVar(&port, "p", 4001, "port") flag.StringVar(&join, "join", "", "host:port of leader to join") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [arguments] \n", os.Args[0]) flag.PrintDefaults() } } func main() { log.SetFlags(0) flag.Parse() if verbose { log.Print("Verbose logging enabled.") } if trace { raft.SetLogLevel(raft.Trace) log.Print("Raft trace debugging enabled.") } else if debug { raft.SetLogLevel(raft.Debug) log.Print("Raft debugging enabled.") } rand.Seed(time.Now().UnixNano()) // Setup commands. raft.RegisterCommand(&command.WriteCommand{}) // Set the data directory. if flag.NArg() == 0 { flag.Usage() log.Fatal("Data path argument required") } path := flag.Arg(0) if err := os.MkdirAll(path, 0744); err != nil { log.Fatalf("Unable to create path: %v", err) } log.SetFlags(log.LstdFlags) s := server.New(path, host, port) log.Fatal(s.ListenAndServe(join)) } ================================================ FILE: server/server.go ================================================ package server import ( "bytes" "encoding/json" "fmt" "github.com/goraft/raft" "github.com/goraft/raftd/command" "github.com/goraft/raftd/db" "github.com/gorilla/mux" "io/ioutil" "log" "math/rand" "net/http" "path/filepath" "sync" "time" ) // The raftd server is a combination of the Raft server and an HTTP // server which acts as the transport. type Server struct { name string host string port int path string router *mux.Router raftServer raft.Server httpServer *http.Server db *db.DB mutex sync.RWMutex } // Creates a new server. func New(path string, host string, port int) *Server { s := &Server{ host: host, port: port, path: path, db: db.New(), router: mux.NewRouter(), } // Read existing name or generate a new one. if b, err := ioutil.ReadFile(filepath.Join(path, "name")); err == nil { s.name = string(b) } else { s.name = fmt.Sprintf("%07x", rand.Int())[0:7] if err = ioutil.WriteFile(filepath.Join(path, "name"), []byte(s.name), 0644); err != nil { panic(err) } } return s } // Returns the connection string. func (s *Server) connectionString() string { return fmt.Sprintf("http://%s:%d", s.host, s.port) } // Starts the server. func (s *Server) ListenAndServe(leader string) error { var err error log.Printf("Initializing Raft Server: %s", s.path) // Initialize and start Raft server. transporter := raft.NewHTTPTransporter("/raft", 200*time.Millisecond) s.raftServer, err = raft.NewServer(s.name, s.path, transporter, nil, s.db, "") if err != nil { log.Fatal(err) } transporter.Install(s.raftServer, s) s.raftServer.Start() if leader != "" { // Join to leader if specified. log.Println("Attempting to join leader:", leader) if !s.raftServer.IsLogEmpty() { log.Fatal("Cannot join with an existing log") } if err := s.Join(leader); err != nil { log.Fatal(err) } } else if s.raftServer.IsLogEmpty() { // Initialize the server by joining itself. log.Println("Initializing new cluster") _, err := s.raftServer.Do(&raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: s.connectionString(), }) if err != nil { log.Fatal(err) } } else { log.Println("Recovered from log") } log.Println("Initializing HTTP server") // Initialize and start HTTP server. s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", s.port), Handler: s.router, } s.router.HandleFunc("/db/{key}", s.readHandler).Methods("GET") s.router.HandleFunc("/db/{key}", s.writeHandler).Methods("POST") s.router.HandleFunc("/join", s.joinHandler).Methods("POST") log.Println("Listening at:", s.connectionString()) return s.httpServer.ListenAndServe() } // This is a hack around Gorilla mux not providing the correct net/http // HandleFunc() interface. func (s *Server) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { s.router.HandleFunc(pattern, handler) } // Joins to the leader of an existing cluster. func (s *Server) Join(leader string) error { command := &raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: s.connectionString(), } var b bytes.Buffer json.NewEncoder(&b).Encode(command) resp, err := http.Post(fmt.Sprintf("http://%s/join", leader), "application/json", &b) if err != nil { return err } resp.Body.Close() return nil } func (s *Server) joinHandler(w http.ResponseWriter, req *http.Request) { command := &raft.DefaultJoinCommand{} if err := json.NewDecoder(req.Body).Decode(&command); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if _, err := s.raftServer.Do(command); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func (s *Server) readHandler(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) value := s.db.Get(vars["key"]) w.Write([]byte(value)) } func (s *Server) writeHandler(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) // Read the value from the POST body. b, err := ioutil.ReadAll(req.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } value := string(b) // Execute the command against the Raft server. _, err = s.raftServer.Do(command.NewWriteCommand(vars["key"], value)) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } }