// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { state := s.cluster.State() if state != "primary" { http.Error(w, "Only the primary can service queries, but this is a "+state, http.StatusBadRequest) return } query, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, err.Error(), http.StatusBadRequest) } log.Debugf("[%s] Received query: %#v", s.cluster.State(), string(query)) resp, err := s.execute(query) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } r := &Replicate{ Self: s.cluster.self, Query: query, } for _, member := range s.cluster.members { b := util.JSONEncode(r) _, err := s.client.SafePost(member.ConnectionString, "/replicate", b) if err != nil { log.Printf("Couldn't replicate query to %v: %s", member, err) } } log.Debugf("[%s] Returning response to %#v: %#v", s.cluster.State(), string(query), string(resp)) w.Write(resp) }
func (s *Server) forwardQueryToLeader(query *sql.Query) { if s.raftServer.Leader() == s.name { // I am the leader, don't send it return } cs, err := s.GetLeaderCS() if err != nil { //log.Printf("Can't find leader to forward query") // Can't find leader return } var buffer bytes.Buffer compress := gzip.NewWriter(&buffer) err = EncodeObjToResponse(compress, query) compress.Close() if err != nil { log.Printf("Can't encode object to forward") } _, err = s.client.SafePost(cs, "/forward", "application/json", &buffer) if err != nil { log.Printf("Failed to forward query") } }
// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { query, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, err.Error(), http.StatusBadRequest) return } log.Debugf("[%s] Received query: %#v", s.raftServer.State(), string(query)) // Execute the command against the Raft server. leader := s.raftServer.Leader() for leader == "" { time.Sleep(50 * time.Millisecond) leader = s.raftServer.Leader() } if s.name != leader { my_partial_name := strings.TrimSuffix(strings.TrimPrefix(s.name, ".-"), ".sock") leader_partial_name := strings.TrimSuffix(strings.TrimPrefix(leader, ".-"), ".sock") redirect_url := "http://" + strings.Replace(req.Host, my_partial_name, leader_partial_name, -1) + "/forward?query=" + encodeQuery(query) log.Printf("Redirecting to %s", redirect_url) http.Redirect(w, req, redirect_url, 302) return } resp, err := s.raftServer.Do(NewSqlCommand(string(query))) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } resp_frd := resp.([]byte) log.Debugf("[%s] Returning response to %#v: %#v", s.raftServer.State(), string(query), string(resp_frd)) w.Write(resp_frd) }
// Join an existing cluster func (s *Server) Join(primary string) error { command := &raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: s.connectionString(), } var b bytes.Buffer json.NewEncoder(&b).Encode(command) cs, err := transport.Encode(primary) if err != nil { return err } log.Printf("Server %v with cs=%v is trying to join %v on %s/join", s.raftServer.Name(), s.connectionString(), primary, cs) for { _, err := s.client.SafePost(cs, "/join", &b) if err != nil { log.Printf("Unable to join cluster: %s", err) time.Sleep(1 * time.Second) continue } return nil } }
// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { if s.block { time.Sleep(1000000 * time.Second) } query, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, err.Error(), http.StatusBadRequest) } if s.leader != s.listen { cs, errLeader := transport.Encode(s.leader) if errLeader != nil { http.Error(w, "Only the primary can service queries, but this is a secondary", http.StatusBadRequest) log.Printf("Leader ain't present?: %s", errLeader) return } //_, errLeaderHealthCheck := s.client.SafeGet(cs, "/healthcheck") //if errLeaderHealthCheck != nil { // http.Error(w, "Primary is down", http.StatusBadRequest) // return //} body, errLResp := s.client.SafePost(cs, "/sql", bytes.NewBufferString(string(query))) if errLResp != nil { s.block = true http.Error(w, "Can't forward request to primary, gotta block now", http.StatusBadRequest) return // log.Printf("Didn't get reply from leader: %s", errLResp) } formatted := fmt.Sprintf("%s", body) resp := []byte(formatted) w.Write(resp) return } else { log.Debugf("Primary Received query: %#v", string(query)) resp, err := s.execute(query) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } w.Write(resp) return } }
func (s *Server) forwardHandler(w http.ResponseWriter, req *http.Request) { log.Printf("############################# forward: Initiating forward loop") h, ok := w.(http.Hijacker) if !ok { log.Printf("########################## forward: Failed to hijack connection for forwarding. Aborting") return } netConn, rw, err := h.Hijack() if err != nil { log.Printf("######################### forward: Failed to hijack connection for forwarding: %v Aborting", err) return } defer netConn.Close() reader := json.NewDecoder(rw) writer := json.NewEncoder(rw) log.Printf("############################ forward: Forward loop initiated") for { cmd := <-s.execCommand log.Printf("############ Received command to forward: %v", cmd.Data) err := writer.Encode(&cmd.Data) if err != nil { log.Printf("######################### forward: Writer cannot encode command: %v Stopping loop", err) return } reply := &raft.ExecuteCommandReply{} err = reader.Decode(reply) if err != nil { log.Printf("############################ forward: Reader cannot decode command reply: %v Stopping loop", err) return } log.Printf("############# Received reply via forward: %v", reply) cmd.Reply <- reply } }
func (c *Cluster) PerformFailover() { state := c.State() if state != "secondary" { log.Fatalf("Trying to fail over even though my state is %s", state) } c.primary = c.members[0] c.members = c.members[1:] if c.State() == "primary" { log.Printf("I am the the primary now.") } else { log.Printf("Promoted %s to primary. My time will come one day.", c.primary.Name) } }
// Server handlers 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 } log.Printf("Handling join request: %#v", command) if _, err := s.raftServer.Do(command); err != nil { log.Printf("Server was unable to join %v with err=%v", command, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } }
func (s *Server) healthCheckPeer(peer *raft.Peer) { for { delay := HEALTH_CHECK_DELAY if s.raftServer.Leader() == s.name { delay = LEADER_HEALTH_CHECK_DELAY } // Endlessly response, err := s.client.SafeGet(peer.ConnectionString, "/healthcheck") if err != nil { continue } healthResponse := &HealthCheckResponse{} // if err := json.NewDecoder(response).Decode(&healthResponse); err != nil { // log.Fatal(err) // return // } err = DecodeResponseToObj(response, &healthResponse) if err != nil { log.Printf("healthCheckPeer Error: %v", err.Error()) continue } s.mergeQueue(healthResponse.Outstanding) time.Sleep(delay) } }
// Join an existing cluster func (s *Server) Join(primary string) error { join := &Join{Self: s.cluster.self} b := util.JSONEncode(join) cs, err := transport.Encode(primary) if err != nil { return err } for { body, err := s.client.SafePost(cs, "/join", b) if err != nil { log.Printf("Unable to join cluster: %s", err) time.Sleep(1 * time.Second) continue } resp := &JoinResponse{} if err = util.JSONDecode(body, &resp); err != nil { return err } s.cluster.Join(resp.Self, resp.Members) return nil } }
// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { state := s.cluster.State() query, err := ioutil.ReadAll(req.Body) if state != "primary" { log.Printf("I AM NOT THE PRIMARY, NEED TO FORWARD THIS REQUEST TO MASTER") //cs, err := transport.Encode(primary) response, err := s.client.SafePost(s.cluster.primary.ConnectionString, "/sql", bytes.NewReader(query)) if response == nil || err != nil { http.Error(w, "", http.StatusBadRequest) return } else { r, _ := ioutil.ReadAll(response) w.Write(r) } //http.Error(w, "Only the primary can service queries, but this is a "+state, http.StatusBadRequest) return } if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, "", http.StatusBadRequest) } log.Debugf("[%s] Received query: %#v", s.cluster.State(), string(query)) resp, err := s.execute(query) if err != nil { http.Error(w, "", http.StatusBadRequest) } //r := &Replicate{ //Self: s.cluster.self, //Query: query, //} //for _, member := range s.cluster.members { //b := util.JSONEncode(r) //_, err := s.client.SafePost(member.ConnectionString, "/replicate", b) //if err != nil { //log.Printf("Couldn't replicate query to %v: %s", member, err) //} //} log.Debugf("[%s] Returning response to %#v: %#v", s.cluster.State(), string(query), string(resp)) w.Write(resp) }
// Server handlers func (s *Server) joinHandler(w http.ResponseWriter, req *http.Request) { log.Printf("handling /join %v", req) j := &raft.DefaultJoinCommand{} if err := util.JSONDecode(req.Body, j); err != nil { log.Printf("Invalid join request: %s", err) return } go func() { log.Printf("Handling join request: %#v", j) // Add node to the cluster if _, err := s.raftServer.Do(j); err != nil { return } }() }
// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { query, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, err.Error(), http.StatusBadRequest) return } log.Printf("[%s]Received query: %#v", string(s.raftServer.State()), string(query)) if s.raftServer.State() != "leader" { if s.raftServer.Leader() == "" { w.WriteHeader(http.StatusBadRequest) return } leader, _ := transport.Encode(s.raftServer.Leader()) log.Printf("Relaying query to leader: %v", s.raftServer.Leader()) relayResp, err := s.client.SafePost(leader, "/sql", bytes.NewReader(query)) if err != nil { http.Error(w, "Only the primary can service queries, relaying failed", http.StatusBadRequest) return } //buf := new(bytes.Buffer) //buf.ReadFrom(relayResp) w.Write(relayResp.(*bytes.Buffer).Bytes()) return } // Execute the command against the Raft server. resp, err := s.raftServer.Do(command.NewWriteCommand(string(query))) if err != nil { log.Printf("Current leader: %#v resp=%v err=%v", string(s.raftServer.Leader()), resp, err) http.Error(w, err.Error(), http.StatusBadRequest) return } if formatted, ok := resp.(string); ok { log.Printf("Returning response to %#v: %#v", string(query), resp) w.Write([]byte(formatted)) } else { w.WriteHeader(http.StatusBadRequest) } }
func (s *Server) healthcheckPrimary() bool { _, err := s.client.SafeGet(s.cluster.primary.ConnectionString, "/healthcheck") if err != nil { log.Printf("The primary appears to be down: %s", err) return false } else { return true } }
func (s *Server) healthcheckHandler(w http.ResponseWriter, req *http.Request) { response := &HealthCheckResponse{ Outstanding: s.outstanding.GetAll(), } err := EncodeObjToResponse(w, response) if err != nil { log.Printf("healthCheckHandler ERror : %v", err.Error()) } }
func (c *Cluster) AddMember(identity ServerAddress) error { state := c.State() if state != "primary" { return errors.New("Can only join to a primary, but you're talking to a " + state) } log.Printf("Adding new cluster member %v", identity) c.members = append(c.members, identity) return nil }
func (s *Server) replicationHandler(w http.ResponseWriter, req *http.Request) { r := &Replicate{} if err := util.JSONDecode(req.Body, r); err != nil { log.Printf("Invalid replication request: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("Handling replication request from %v", r.Self) _, err := s.execute(r.Query) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } resp := &ReplicateResponse{ s.cluster.self, } b := util.JSONEncode(resp) w.Write(b.Bytes()) }
// Server handlers func (s *Server) joinHandler(w http.ResponseWriter, req *http.Request) { command := &raft.DefaultJoinCommand{} if err := json.NewDecoder(req.Body).Decode(&command); err != nil { log.Printf("Invalid join request: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } result, err := s.raftServer.Do(command) if err != nil { log.Printf("Unable to handle join: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } if b, ok := result.([]byte); ok { log.Printf(string(b)) w.WriteHeader(http.StatusOK) w.Write(b) } }
// Server handlers func (s *Server) joinHandler(w http.ResponseWriter, req *http.Request) { j := &Join{} if err := util.JSONDecode(req.Body, j); err != nil { log.Printf("Invalid join request: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("Handling join request: %#v", j) // Add node to the cluster if err := s.cluster.AddMember(j.Self); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } // Respond with the current cluster description resp := &JoinResponse{ s.cluster.self, s.cluster.members, } b := util.JSONEncode(resp) w.Write(b.Bytes()) }
// Join an existing cluster func (s *Server) Join(primary string) error { command := &raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: s.connectionString, } b := util.JSONEncode(command) log.Printf("Joining Cluster: %s", primary) cs, err := transport.Encode(primary) if err != nil { return err } for i := 0; i < 5; i += 1 { log.Printf("Join -> %s Command: %s", cs, command) _, err = s.client.SafePost(cs, "/join", b) if err != nil { log.Printf("Unable to join cluster: %s", err) } else { break } } return err }
func (sql *SQL) Execute(tag string, command string) (*Output, error) { // TODO: make sure I can catch non-lock issuez sql.mutex.Lock() defer sql.mutex.Unlock() defer func() { sql.sequenceNumber += 1 }() if tag == "primary" || log.Verbose() { log.Printf("[%s] [%d] Executing %#v", tag, sql.sequenceNumber, command) } subprocess := exec.Command("sqlite3", sql.path) subprocess.Stdin = strings.NewReader(command + ";") var stdout, stderr bytes.Buffer subprocess.Stdout = &stdout subprocess.Stderr = &stderr if err := subprocess.Start(); err != nil { log.Panic(err) } var o, e []byte if err := subprocess.Wait(); err != nil { exitstatus := getExitstatus(err) switch true { case exitstatus < 0: log.Panic(err) case exitstatus == 1: fallthrough case exitstatus == 2: o = stderr.Bytes() e = nil } } else { o = stdout.Bytes() e = stderr.Bytes() } output := &Output{ Stdout: o, Stderr: e, SequenceNumber: sql.sequenceNumber, } return output, nil }
// Creates a new server. func New(path, listen string) (*Server, error) { cs, err := transport.Encode(listen) if err != nil { return nil, err } log.Printf("My connection string is %s", cs) sqlPath := filepath.Join(path, "storage.sql") util.EnsureAbsent(sqlPath) s := &Server{ path: path, name: strings.Replace(listen, "/", "-", -1), listen: listen, connection_string: cs, sql: sql.NewSQL(sqlPath), router: mux.NewRouter(), client: transport.NewClient(), } return s, nil }
// Creates a new server. func New(path, listen string) (*Server, error) { cs, err := transport.Encode(listen) if err != nil { return nil, err } log.Printf("Starting server at" + cs) sqlPath := filepath.Join(path, "storage.sql") util.EnsureAbsent(sqlPath) s := &Server{ path: path, listen: listen, sql: sql.NewSQL(sqlPath), router: mux.NewRouter(), client: transport.NewClient(), block: false, } return s, nil }
// Join an existing cluster func (s *Server) Join(primary string) error { cs, err := transport.Encode(primary) if err != nil { return err } command := &raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: s.connectionString(), } for { b := util.JSONEncode(command) _, err = s.client.SafePost(cs, "/join", b) if err != nil { log.Printf("Unable to join cluster: %s", err) time.Sleep(1 * time.Second) continue } return nil } return nil }
func main() { //defer profile.Start(profile.CPUProfile).Stop() var raftdebug, verbose bool var listen, join, directory string flag.BoolVar(&verbose, "v", false, "Enable debug output") flag.BoolVar(&raftdebug, "r", false, "Enable raft debugging") flag.StringVar(&listen, "l", "127.0.0.1:4000", "Socket to listen on (Unix or TCP)") flag.StringVar(&join, "join", "", "Cluster to join") flag.StringVar(&directory, "d", "", "Storage directory") dir := filepath.Dir(os.Args[0]) base := "./" + filepath.Base(os.Args[0]) flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: %s [options] SQLCluster is a highly-available SQL store. It accepts commands over HTTP, and returns the output of commands together with a SequenceNumber which indicates the ordering in which requests are being applied. (SequenceNumber is useful for validation, and can otherwise be ignored.) Run a cluster as follows: cd %s %s -d /tmp/sqlcluster/node0 & %s -d /tmp/sqlcluster/node1 -l 127.0.0.1:4001 --join 127.0.0.1:4000 & %s -d /tmp/sqlcluster/node2 -l 127.0.0.1:4002 --join 127.0.0.1:4000 You can then issue queries using your favorite HTTP client. curl 127.0.0.1:4000/sql -d 'CREATE TABLE hello (world int)' curl 127.0.0.1:4000/sql -d 'INSERT INTO hello (world) VALUES (1), (2)' curl 127.0.0.1:4000/sql -d 'SELECT * FROM hello' This should return the following sequence of outputs: - SequenceNumber: 0 - SequenceNumber: 1 - SequenceNumber: 2 1 2 By default, SQLCluster will listen on a TCP port. However, if you specify a listen address that begins with a / or ., that will be interpeted as Unix path for SQLCluster to listen on. (Note that Octopus runs using Unix sockets only, but it will probably be more convient for you to develop using TCP.) OPTIONS: `, os.Args[0], dir, base, base, base) flag.PrintDefaults() } flag.Parse() if flag.NArg() != 0 { flag.Usage() os.Exit(1) } log.SetVerbose(verbose) if directory == "" { var err error directory, err = ioutil.TempDir("/tmp", "node") if err != nil { log.Fatalf("Could not create temporary base directory: %s", err) } defer os.RemoveAll(directory) log.Printf("Storing state in tmpdir %s", directory) } else { if err := os.MkdirAll(directory, os.ModeDir|0755); err != nil { log.Fatalf("Error while creating storage directory: %s", err) } } log.Printf("Changing directory to %s", directory) if err := os.Chdir(directory); err != nil { log.Fatalf("Error while changing to storage directory: %s", err) } // Make sure we don't leave stranded sqlclusters lying around go func() { for { time.Sleep(2 * time.Second) if os.Getppid() == 1 { log.Fatal("Parent process exited; terminating") } } }() runtime.GOMAXPROCS(4) rand.Seed(time.Now().UnixNano()) raft.RegisterCommand(&server.WriteCommand{}) if raftdebug { raft.SetLogLevel(raft.Debug) } // raft.SetLogLevel(raft.Trace) // Start the server go func() { s, err := server.New(directory, listen) if err != nil { log.Fatal(err) } if err := s.ListenAndServe(join); err != nil { log.Fatal(err) } }() // Exit cleanly so we can remove the tmpdir sigchan := make(chan os.Signal) signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) <-sigchan }
// This is the only user-facing function, and accordingly the body is // a raw string rather than JSON. func (s *Server) sqlHandler(w http.ResponseWriter, req *http.Request) { // Assign id s.mutex.Lock() var id string; var forwarded bool; id = req.URL.Query().Get("id") resChannel := make(chan string) if id == "" { // This is a client talking to us, assign a unique id id = fmt.Sprintf("%s-%d", s.connectionString(), s.counter) s.counter += 1 forwarded = false s.sql.AddClientRecord(id,resChannel); log.Printf("Received request %s",id) } else { // This is a forwarded request forwarded = true if val, ok := s.received[id]; ok && val { s.mutex.Unlock() return } log.Printf("Received forwarded request %s",id) s.received[id]=true w.WriteHeader(http.StatusOK) } s.mutex.Unlock() // Read body query, err := ioutil.ReadAll(req.Body) if err != nil { log.Printf("Couldn't read body: %s", err) http.Error(w, err.Error(), http.StatusBadRequest) } // Forward if we are not the leader go func() { var count int = 0 delay := 50 * time.Millisecond for { count += 1 if count > 1 { log.Printf("Retry count for %s is %d",id,count) time.Sleep(delay) if (delay < 200*time.Millisecond) { delay *= 2 } } // Redirect if we are not the leader if !s.IsLeader() { // Find leader and make sure we do not redirect back to us target := s.LeaderConnectionString() if (target == "") { continue } if (target == s.name) { continue } // Redirect for { res, err := s.client.RawPost(target, fmt.Sprintf("/sql?id=%s",id), bytes.NewBuffer(query)) if err == nil && res.StatusCode == 200 { break } time.Sleep(50 * time.Millisecond) //if (delay < 200*time.Millisecond) { delay *= 2 } } s.mutex.Lock() s.received[id] = false s.mutex.Unlock() break } else { _, err := s.raftServer.Do(NewWriteCommand(id,string(query))) // Retry on error if err != nil { continue; } break } } }() // Respond if we orignally received the request from the client on this ndoe if !forwarded { response := <- resChannel log.Printf("Responding for id %s",id) w.Write([]byte(response)) } }
// 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") transporter.Transport.Dial = transport.UnixDialer s.raftServer, err = raft.NewServer(s.name, s.path, transporter, nil, s.sql, "") if err != nil { log.Fatal(err) } transporter.Install(s.raftServer, s) s.raftServer.Start() s.raftServer.SetHeartbeatTimeout(1 * time.Millisecond) s.raftServer.SetElectionTimeout(500 * time.Millisecond) fn := func(e raft.Event) { log.Printf("%s %v -> %v\n", e.Type(), e.PrevValue(), e.Value()) } s.raftServer.AddEventListener(raft.StateChangeEventType, fn) s.raftServer.AddEventListener(raft.LeaderChangeEventType, fn) s.raftServer.AddEventListener(raft.TermChangeEventType, fn) 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") } //time.Sleep(1 * time.Second) if err := s.Join(leader); err != nil { log.Println("Join failed") log.Fatal(err) } log.Printf("Node %s joined leader %s" , s.connectionString(), leader) } else if s.raftServer.IsLogEmpty() { // Initialize the server by joining itself. log.Println("Initializing new cluster") cs, err := transport.Encode(s.listen) if err != nil { return err } _, err = s.raftServer.Do(&raft.DefaultJoinCommand{ Name: s.raftServer.Name(), ConnectionString: cs, }) 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{ Handler: s.router, } s.router.HandleFunc("/sql", s.sqlHandler).Methods("POST") s.router.HandleFunc("/join", s.joinHandler).Methods("POST") // Start Unix transport l, err := transport.Listen(s.listen) if err != nil { log.Fatal(err) } return s.httpServer.Serve(l) }
func Listen(addr string) (net.Listener, error) { network := Network(addr) log.Printf("Listening on %s: %s", network, addr) return net.Listen(network, addr) }
func (c *Cluster) Init() { log.Printf("Initializing cluster and promoting self to primary") c.primary = c.self c.members = make([]ServerAddress, 0) }
func (c *Cluster) Join(primary ServerAddress, members []ServerAddress) { log.Printf("Joining existing cluster: primary %v, members %v", primary, members) c.primary = primary c.members = members }