github.com/mailgun/holster/v4@v4.20.0/cmd/election/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"os/signal"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/mailgun/holster/v4/discovery"
    16  	"github.com/mailgun/holster/v4/election"
    17  	"github.com/mailgun/holster/v4/errors"
    18  	"github.com/sirupsen/logrus"
    19  )
    20  
    21  func sendRPC(ctx context.Context, peer string, req election.RPCRequest, resp *election.RPCResponse) error {
    22  	// Marshall the RPC request to json
    23  	b, err := json.Marshal(req)
    24  	if err != nil {
    25  		return errors.Wrap(err, "while encoding request")
    26  	}
    27  
    28  	// Create a new http request with context
    29  	hr, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("http://%s/rpc", peer), bytes.NewBuffer(b))
    30  	if err != nil {
    31  		return errors.Wrap(err, "while creating request")
    32  	}
    33  
    34  	// Send the request
    35  	hp, err := http.DefaultClient.Do(hr)
    36  	if err != nil {
    37  		return errors.Wrap(err, "while sending http request")
    38  	}
    39  	defer func() {
    40  		_ = hp.Body.Close()
    41  	}()
    42  
    43  	// Decode the response from JSON
    44  	dec := json.NewDecoder(hp.Body)
    45  	if err := dec.Decode(&resp); err != nil {
    46  		return errors.Wrap(err, "while decoding response")
    47  	}
    48  	return nil
    49  }
    50  
    51  func newHandler(node election.Node) func(w http.ResponseWriter, r *http.Request) {
    52  	return func(w http.ResponseWriter, r *http.Request) {
    53  		dec := json.NewDecoder(r.Body)
    54  		var req election.RPCRequest
    55  		if err := dec.Decode(&req); err != nil {
    56  			status := http.StatusBadRequest
    57  			w.WriteHeader(status)
    58  			if _, err := w.Write([]byte(err.Error())); err != nil {
    59  				logrus.WithError(err).WithField("status", status).Warn("while writing response")
    60  			}
    61  		}
    62  
    63  		// Example of how a peer might exclude RPC
    64  		// commands it doesn't want made.
    65  		if req.RPC == election.SetPeersRPC {
    66  			status := http.StatusBadRequest
    67  			w.WriteHeader(status)
    68  			if _, err := fmt.Fprintf(w, "RPC request '%s' not allowed", req.RPC); err != nil {
    69  				logrus.WithError(err).WithField("status", status).Warn("while writing response")
    70  			}
    71  			return
    72  		}
    73  
    74  		var resp election.RPCResponse
    75  		node.ReceiveRPC(req, &resp)
    76  
    77  		enc := json.NewEncoder(w)
    78  		if err := enc.Encode(resp); err != nil {
    79  			status := http.StatusInternalServerError
    80  			w.WriteHeader(status)
    81  			if _, err = w.Write([]byte(err.Error())); err != nil {
    82  				logrus.WithError(err).WithField("status", status).Warn("while writing response")
    83  			}
    84  		}
    85  	}
    86  }
    87  
    88  func main() {
    89  	if len(os.Args) != 4 {
    90  		logrus.Fatal("usage: <election-address:8080> <memberlist-address:8180> <known-address:8180>")
    91  	}
    92  
    93  	electionAddr, memberListAddr, knownAddr := os.Args[1], os.Args[2], os.Args[3]
    94  	// logrus.SetLevel(logrus.DebugLevel)
    95  
    96  	node, err := election.NewNode(election.Config{
    97  		// A unique identifier used to identify us in a list of peers
    98  		UniqueID: electionAddr,
    99  		// Called whenever the library detects a change in leadership
   100  		OnUpdate: func(leader string) {
   101  			logrus.Printf("Current Leader: %s\n", leader)
   102  		},
   103  		// Called when the library wants to contact other peers
   104  		SendRPC: sendRPC,
   105  	})
   106  	if err != nil {
   107  		logrus.Fatal(err)
   108  	}
   109  
   110  	// Create a member list catalog
   111  	ml, err := discovery.NewMemberList(context.Background(), discovery.MemberListConfig{
   112  		BindAddress: memberListAddr,
   113  		Peer: discovery.Peer{
   114  			ID:       uuid.New().String(),
   115  			Metadata: []byte(electionAddr),
   116  		},
   117  		KnownPeers: []string{knownAddr},
   118  		OnUpdate: func(peers []discovery.Peer) {
   119  			var result []string
   120  			for _, p := range peers {
   121  				result = append(result, string(p.Metadata))
   122  			}
   123  			logrus.Infof("Update Peers: %s", result)
   124  			if err := node.SetPeers(context.Background(), result); err != nil {
   125  				logrus.WithError(err).Warn("while setting peers")
   126  			}
   127  		},
   128  	})
   129  	if err != nil {
   130  		logrus.Fatal(err)
   131  	}
   132  
   133  	mux := http.NewServeMux()
   134  	mux.HandleFunc("/rpc", newHandler(node))
   135  	go func() {
   136  		server := &http.Server{
   137  			Addr:              electionAddr,
   138  			Handler:           mux,
   139  			ReadHeaderTimeout: 1 * time.Minute,
   140  		}
   141  		logrus.Fatal(server.ListenAndServe())
   142  	}()
   143  
   144  	// Wait until the http server is up and can receive RPC requests
   145  	if err := election.WaitForConnect(electionAddr, 10, time.Millisecond*100); err != nil {
   146  		logrus.Fatal(err)
   147  	}
   148  
   149  	// Now that our http handler is listening for requests we
   150  	// can safely start the election.
   151  	if err := node.Start(context.Background()); err != nil {
   152  		logrus.Fatal(err)
   153  	}
   154  
   155  	// Wait here for signals to clean up our mess
   156  	c := make(chan os.Signal, 1)
   157  	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   158  	for range c {
   159  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   160  		if err := ml.Close(ctx); err != nil {
   161  			logrus.WithError(err).Error("during member list catalog close")
   162  		}
   163  		cancel()
   164  		_ = node.Stop(context.Background())
   165  		os.Exit(0)
   166  	}
   167  }