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 }