github.com/tiagovtristao/plz@v13.4.0+incompatible/src/worker/worker.go (about)

     1  // Package worker implements functions for communicating with subordinate worker processes.
     2  package worker
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os/exec"
    10  	"strings"
    11  	"sync"
    12  
    13  	"gopkg.in/op/go-logging.v1"
    14  
    15  	"github.com/thought-machine/please/src/core"
    16  )
    17  
    18  var log = logging.MustGetLogger("worker")
    19  
    20  // A workerServer is the structure we use to maintain information about a remote work server.
    21  type workerServer struct {
    22  	requests      chan *Request
    23  	responses     map[string]chan *Response
    24  	responseMutex sync.Mutex
    25  	process       *exec.Cmd
    26  	stderr        *stderrLogger
    27  	closing       bool
    28  }
    29  
    30  // workerMap contains all the remote workers we've started so far.
    31  var workerMap = map[string]*workerServer{}
    32  var workerMutex sync.Mutex
    33  
    34  // BuildRemotely runs a single build request and returns its response.
    35  func BuildRemotely(state *core.BuildState, target *core.BuildTarget, worker string, req *Request) (*Response, error) {
    36  	return buildRemotely(state, target, worker, "building (using "+worker+")", req)
    37  }
    38  
    39  func buildRemotely(state *core.BuildState, target *core.BuildTarget, worker, msg string, req *Request) (*Response, error) {
    40  	w, err := getOrStartWorker(state, worker)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	ch := make(chan *Response, 2)
    45  	w.responseMutex.Lock()
    46  	w.responses[req.Rule] = ch
    47  	w.responseMutex.Unlock()
    48  
    49  	if target != nil {
    50  		ctx, cancel := context.WithCancel(context.Background())
    51  		defer cancel()
    52  		go core.LogProgress(ctx, target, msg)
    53  	}
    54  
    55  	// Time out this request appropriately
    56  	ctx, cancel := context.WithTimeout(context.Background(), core.TargetTimeoutOrDefault(target, state))
    57  	defer cancel()
    58  	w.requests <- req
    59  	select {
    60  	case response := <-ch:
    61  		return response, nil
    62  	case <-ctx.Done():
    63  		return nil, ctx.Err()
    64  	}
    65  }
    66  
    67  // ProvideParse sends a request to a subprocess to derive pseudo-contents of a BUILD file from
    68  // a directory (e.g. they may infer it from file contents).
    69  // If the provider cannot infer anything, they will return an empty string.
    70  func ProvideParse(state *core.BuildState, worker string, dir string) (string, error) {
    71  	w, err := getOrStartWorker(state, worker)
    72  	if err != nil {
    73  		return "", err
    74  	}
    75  	w.requests <- &Request{
    76  		Rule: dir,
    77  	}
    78  	ch := make(chan *Response, 1)
    79  	w.responseMutex.Lock()
    80  	w.responses[dir] = ch
    81  	w.responseMutex.Unlock()
    82  	response := <-ch
    83  	return response.BuildFile, nil
    84  }
    85  
    86  // EnsureWorkerStarted ensures that a worker server is started and has responded saying it's ready.
    87  func EnsureWorkerStarted(state *core.BuildState, worker string, target *core.BuildTarget) error {
    88  	resp, err := buildRemotely(state, target, worker, "waiting for "+worker+" to start", &Request{
    89  		Rule: target.Label.String(),
    90  		Test: true,
    91  	})
    92  	if err == nil && !resp.Success {
    93  		return fmt.Errorf(strings.Join(resp.Messages, "\n"))
    94  	}
    95  	return err
    96  }
    97  
    98  // getOrStartWorker either retrieves an existing worker process or starts a new one.
    99  func getOrStartWorker(state *core.BuildState, worker string) (*workerServer, error) {
   100  	workerMutex.Lock()
   101  	defer workerMutex.Unlock()
   102  	if w, present := workerMap[worker]; present {
   103  		return w, nil
   104  	}
   105  	// Need to create a new process
   106  	cmd := core.ExecCommand(worker)
   107  	cmd.Env = core.GeneralBuildEnvironment(state.Config)
   108  	stdin, err := cmd.StdinPipe()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	stdout, err := cmd.StdoutPipe()
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	stderr := &stderrLogger{}
   117  	cmd.Stderr = stderr
   118  	if err := cmd.Start(); err != nil {
   119  		return nil, err
   120  	}
   121  	w := &workerServer{
   122  		requests:  make(chan *Request),
   123  		responses: map[string]chan *Response{},
   124  		process:   cmd,
   125  		stderr:    stderr,
   126  	}
   127  	workerMap[worker] = w
   128  	go w.sendRequests(stdin)
   129  	go w.readResponses(stdout)
   130  	go w.wait()
   131  	state.Stats.NumWorkerProcesses = len(workerMap)
   132  	return w, nil
   133  }
   134  
   135  // sendRequests sends requests to a running worker server.
   136  func (w *workerServer) sendRequests(stdin io.Writer) {
   137  	e := json.NewEncoder(stdin)
   138  	for request := range w.requests {
   139  		if err := e.Encode(request); err != nil {
   140  			log.Error("Failed to write request: %s", err)
   141  			w.dispatchResponse(&Response{
   142  				Rule:     request.Rule,
   143  				Success:  false,
   144  				Messages: []string{err.Error()},
   145  			})
   146  			continue
   147  		}
   148  		stdin.Write([]byte{'\n'}) // Newline delimit them as a nicety.
   149  	}
   150  }
   151  
   152  // readResponses reads the responses from a running worker server and dispatches them appropriately.
   153  func (w *workerServer) readResponses(stdout io.Reader) {
   154  	decoder := json.NewDecoder(stdout)
   155  	for {
   156  		response := Response{}
   157  		if err := decoder.Decode(&response); err != nil {
   158  			w.Error("Failed to read response: %s", err)
   159  			break
   160  		}
   161  		w.dispatchResponse(&response)
   162  	}
   163  }
   164  
   165  // dispatchResponse sends a single response on the appropriate channel.
   166  func (w *workerServer) dispatchResponse(response *Response) {
   167  	w.responseMutex.Lock()
   168  	ch, present := w.responses[response.Rule]
   169  	delete(w.responses, response.Rule)
   170  	w.responseMutex.Unlock()
   171  	if present {
   172  		log.Debug("Got response from remote worker for %s, success: %v", response.Rule, response.Success)
   173  		ch <- response
   174  	} else {
   175  		w.Error("Couldn't find response channel for %s", response.Rule)
   176  	}
   177  }
   178  
   179  // wait waits for the process to terminate. If it dies unexpectedly this handles various failures.
   180  func (w *workerServer) wait() {
   181  	if err := w.process.Wait(); !w.closing {
   182  		if err != nil {
   183  			log.Error("Worker process died unexpectedly: %s", err)
   184  		} else {
   185  			log.Error("Worker process terminated unexpectedly")
   186  		}
   187  		w.responseMutex.Lock()
   188  		defer w.responseMutex.Unlock()
   189  		for label, ch := range w.responses {
   190  			ch <- &Response{
   191  				Rule:     label,
   192  				Messages: []string{fmt.Sprintf("Worker failed: %s\n%s", err, string(w.stderr.History))},
   193  			}
   194  		}
   195  	}
   196  }
   197  
   198  func (w *workerServer) Error(msg string, args ...interface{}) {
   199  	if !w.closing {
   200  		log.Error(msg, args...)
   201  	}
   202  }
   203  
   204  // stderrLogger is used to log any errors from our worker tools.
   205  type stderrLogger struct {
   206  	buffer  []byte
   207  	History []byte
   208  	// suppress will silence any further logging messages when set.
   209  	Suppress bool
   210  }
   211  
   212  // Write implements the io.Writer interface
   213  func (l *stderrLogger) Write(msg []byte) (int, error) {
   214  	l.buffer = append(l.buffer, msg...)
   215  	if len(l.buffer) > 0 && l.buffer[len(l.buffer)-1] == '\n' {
   216  		if !l.Suppress {
   217  			if msg := strings.TrimSpace(string(l.buffer)); strings.HasPrefix(msg, "WARNING") {
   218  				log.Warning("Warning from remote worker: %s", msg)
   219  			} else {
   220  				log.Error("Error from remote worker: %s", msg)
   221  			}
   222  		}
   223  		l.History = append(l.History, l.buffer...)
   224  		l.buffer = nil
   225  	}
   226  	return len(msg), nil
   227  }
   228  
   229  // StopAll stops any running worker processes.
   230  // This should be called before the process terminates to ensure they are all correctly cleaned up.
   231  func StopAll() {
   232  	for name, worker := range workerMap {
   233  		log.Debug("Terminating build worker %s", name)
   234  		worker.closing = true         // suppress any error messages from worker
   235  		worker.stderr.Suppress = true // Make sure we don't print anything as they die.
   236  		core.KillProcess(worker.process)
   237  	}
   238  	workerMap = map[string]*workerServer{}
   239  }