github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/raft/raftforwarder/worker.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package raftforwarder
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/hashicorp/raft"
    10  	"github.com/juju/errors"
    11  	"github.com/juju/pubsub"
    12  	"gopkg.in/juju/worker.v1"
    13  	"gopkg.in/juju/worker.v1/catacomb"
    14  
    15  	"github.com/juju/juju/core/raftlease"
    16  )
    17  
    18  const applyTimeout = 5 * time.Second
    19  
    20  // This worker receives raft commands forwarded over the hub and
    21  // applies them to the raft node.
    22  
    23  // RaftApplier allows applying a command to the raft FSM.
    24  type RaftApplier interface {
    25  	Apply(cmd []byte, timeout time.Duration) raft.ApplyFuture
    26  }
    27  
    28  // Logger specifies the interface we use from loggo.Logger.
    29  type Logger interface {
    30  	Errorf(string, ...interface{})
    31  	Warningf(string, ...interface{})
    32  	Tracef(string, ...interface{})
    33  }
    34  
    35  // Config defines the resources the worker needs to run.
    36  type Config struct {
    37  	Hub    *pubsub.StructuredHub
    38  	Raft   RaftApplier
    39  	Logger Logger
    40  	Topic  string
    41  	Target raftlease.NotifyTarget
    42  }
    43  
    44  // Validate checks that this config can be used.
    45  func (config Config) Validate() error {
    46  	if config.Hub == nil {
    47  		return errors.NotValidf("nil Hub")
    48  	}
    49  	if config.Raft == nil {
    50  		return errors.NotValidf("nil Raft")
    51  	}
    52  	if config.Logger == nil {
    53  		return errors.NotValidf("nil Logger")
    54  	}
    55  	if config.Topic == "" {
    56  		return errors.NotValidf("empty Topic")
    57  	}
    58  	if config.Target == nil {
    59  		return errors.NotValidf("nil Target")
    60  	}
    61  	return nil
    62  }
    63  
    64  // NewWorker creates and starts a worker that will forward leadership
    65  // claims from non-raft-leader machines.
    66  func NewWorker(config Config) (worker.Worker, error) {
    67  	if err := config.Validate(); err != nil {
    68  		return nil, errors.Trace(err)
    69  	}
    70  	w := &forwarder{
    71  		config: config,
    72  	}
    73  	unsubscribe, err := w.config.Hub.Subscribe(w.config.Topic, w.handleRequest)
    74  	if err != nil {
    75  		return nil, errors.Annotatef(err, "subscribing to %q", w.config.Topic)
    76  	}
    77  	w.unsubscribe = unsubscribe
    78  	if err := catacomb.Invoke(catacomb.Plan{
    79  		Site: &w.catacomb,
    80  		Work: w.loop,
    81  	}); err != nil {
    82  		unsubscribe()
    83  		return nil, errors.Trace(err)
    84  	}
    85  	return w, nil
    86  }
    87  
    88  type forwarder struct {
    89  	catacomb    catacomb.Catacomb
    90  	config      Config
    91  	unsubscribe func()
    92  	id          int
    93  }
    94  
    95  // Kill is part of the worker.Worker interface.
    96  func (w *forwarder) Kill() {
    97  	w.catacomb.Kill(nil)
    98  }
    99  
   100  // Wait is part of the worker.Worker interface.
   101  func (w *forwarder) Wait() error {
   102  	return w.catacomb.Wait()
   103  }
   104  
   105  func (w *forwarder) loop() error {
   106  	defer w.unsubscribe()
   107  	<-w.catacomb.Dying()
   108  	return w.catacomb.ErrDying()
   109  }
   110  
   111  func (w *forwarder) handleRequest(_ string, req raftlease.ForwardRequest, err error) {
   112  	w.id++
   113  	reqID := w.id
   114  	w.config.Logger.Tracef("%d: received %#v, err: %s", reqID, req, err)
   115  	if err != nil {
   116  		// This should never happen, so treat it as fatal.
   117  		w.catacomb.Kill(errors.Annotate(err, "requests callback failed"))
   118  		return
   119  	}
   120  	go func() {
   121  		defer w.config.Logger.Tracef("%d: done", reqID)
   122  		response, err := w.processRequest(req.Command)
   123  		if err != nil {
   124  			w.catacomb.Kill(errors.Annotate(err, "applying command"))
   125  			return
   126  		}
   127  		_, err = w.config.Hub.Publish(req.ResponseTopic, response)
   128  		if err != nil {
   129  			w.catacomb.Kill(errors.Annotate(err, "publishing response"))
   130  			return
   131  		}
   132  	}()
   133  }
   134  
   135  func (w *forwarder) processRequest(command string) (raftlease.ForwardResponse, error) {
   136  	var empty raftlease.ForwardResponse
   137  	future := w.config.Raft.Apply([]byte(command), applyTimeout)
   138  	if err := future.Error(); err != nil {
   139  		return empty, errors.Trace(err)
   140  	}
   141  	respValue := future.Response()
   142  	response, ok := respValue.(raftlease.FSMResponse)
   143  	if !ok {
   144  		return empty, errors.Errorf("expected an FSMResponse, got %T: %#v", respValue, respValue)
   145  	}
   146  	response.Notify(w.config.Target)
   147  	return responseFromError(response.Error()), nil
   148  }
   149  
   150  func responseFromError(err error) raftlease.ForwardResponse {
   151  	return raftlease.ForwardResponse{
   152  		Error: raftlease.AsResponseError(err),
   153  	}
   154  }