github.com/pachyderm/pachyderm@v1.13.4/src/server/worker/pipeline/common.go (about) 1 package pipeline 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "io" 8 "io/ioutil" 9 "os" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/pachyderm/pachyderm/src/client" 15 "github.com/pachyderm/pachyderm/src/client/pfs" 16 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 17 "github.com/pachyderm/pachyderm/src/client/pps" 18 "github.com/pachyderm/pachyderm/src/server/pkg/backoff" 19 "github.com/pachyderm/pachyderm/src/server/pkg/errutil" 20 "github.com/pachyderm/pachyderm/src/server/pkg/ppsconsts" 21 "github.com/pachyderm/pachyderm/src/server/worker/common" 22 "github.com/pachyderm/pachyderm/src/server/worker/driver" 23 "github.com/pachyderm/pachyderm/src/server/worker/logs" 24 ) 25 26 func openAndWait() error { 27 // at the end of file, we open the pipe again, since this blocks until something is written to the pipe 28 openAndWait, err := os.Open("/pfs/out") 29 if err != nil { 30 return err 31 } 32 // and then we immediately close this reader of the pipe, so that the main reader can continue its standard behavior 33 err = openAndWait.Close() 34 if err != nil { 35 return err 36 } 37 return nil 38 } 39 40 // RunUserCode will run the pipeline's user code until canceled by the context 41 // - used for services and spouts. Unlike how the transform worker runs user 42 // code, this does not set environment variables or collect stats. 43 func RunUserCode( 44 driver driver.Driver, 45 logger logs.TaggedLogger, 46 outputCommit *pfs.Commit, 47 inputs []*common.Input, 48 ) error { 49 return backoff.RetryUntilCancel(driver.PachClient().Ctx(), func() error { 50 // TODO: what about the user error handling code? 51 env := driver.UserCodeEnv(logger.JobID(), outputCommit, inputs) 52 return driver.RunUserCode(logger, env, &pps.ProcessStats{}, nil) 53 }, backoff.NewInfiniteBackOff(), func(err error, d time.Duration) error { 54 logger.Logf("error in RunUserCode: %+v, retrying in: %+v", err, d) 55 return nil 56 }) 57 } 58 59 // ReceiveSpout is used by both services and spouts if a spout is defined on the 60 // pipeline. ctx is separate from pachClient because services may call this, and 61 // they use a cancel function that affects the context but not the pachClient 62 // (so metadata updates can still be made while unwinding). 63 func ReceiveSpout( 64 ctx context.Context, 65 pachClient *client.APIClient, 66 pipelineInfo *pps.PipelineInfo, 67 logger logs.TaggedLogger, 68 ) (retErr error) { 69 // Open a read connection to the /pfs/out named pipe. 70 out, err := os.Open("/pfs/out") 71 if err != nil { 72 return err 73 } 74 defer func() { 75 if err := out.Close(); retErr == nil { 76 retErr = err 77 } 78 }() 79 cancelCtx, cancel := context.WithCancel(ctx) 80 repo := pipelineInfo.Pipeline.Name 81 for { 82 if err := withTmpFile("pachyderm_spout_commit", func(f *os.File) error { 83 if err := getNextTarStream(f, out); err != nil { 84 return err 85 } 86 return withSpoutCommit(cancelCtx, pachClient, pipelineInfo, logger, f, func(commit *pfs.Commit, tr *tar.Reader) (retErr error) { 87 putFileClient, err := pachClient.NewPutFileClient() 88 if err != nil { 89 return err 90 } 91 defer func() { 92 if err := putFileClient.Close(); retErr == nil { 93 retErr = err 94 } 95 }() 96 97 for { 98 hdr, err := tr.Next() 99 if err != nil { 100 if errors.Is(err, io.EOF) { 101 return nil 102 } 103 return err 104 } 105 106 if pipelineInfo.Spout.Marker != "" && strings.HasPrefix(path.Clean(hdr.Name), pipelineInfo.Spout.Marker) { 107 // Check to see if this spout is the latest version of this spout by seeing if its spec commit has any children. 108 // TODO: There is a race condition here where the spout could be updated after this check, but before the PutFileOverwrite. 109 spec, err := pachClient.InspectCommit(ppsconsts.SpecRepo, pipelineInfo.SpecCommit.ID) 110 if err != nil && !errutil.IsNotFoundError(err) { 111 return err 112 } 113 if spec != nil && len(spec.ChildCommits) != 0 { 114 cancel() 115 return errors.New("outdated spout, now shutting down") 116 } 117 if _, err := putFileClient.PutFileOverwrite(repo, ppsconsts.SpoutMarkerBranch, hdr.Name, tr, 0); err != nil { 118 return err 119 } 120 // continue rather than putting the file in the output branch 121 continue 122 } 123 if pipelineInfo.Spout.Overwrite { 124 if _, err := putFileClient.PutFileOverwrite(repo, commit.ID, hdr.Name, tr, 0); err != nil { 125 return err 126 } 127 } else { 128 if _, err := putFileClient.PutFile(repo, commit.ID, hdr.Name, tr); err != nil { 129 return err 130 } 131 } 132 } 133 }) 134 }); err != nil { 135 return err 136 } 137 } 138 } 139 140 // TODO: Refactor into a file util package. 141 func withTmpFile(name string, cb func(*os.File) error) (retErr error) { 142 if err := os.MkdirAll(os.TempDir(), 0700); err != nil { 143 return err 144 } 145 f, err := ioutil.TempFile(os.TempDir(), name) 146 if err != nil { 147 return err 148 } 149 defer func() { 150 if err := os.Remove(f.Name()); retErr == nil { 151 retErr = err 152 } 153 if err := f.Close(); retErr == nil { 154 retErr = err 155 } 156 }() 157 return cb(f) 158 } 159 160 func getNextTarStream(w io.Writer, r io.Reader) error { 161 var hdr *tar.Header 162 var err error 163 tr := tar.NewReader(newSkipReader(r)) 164 for { 165 hdr, err = tr.Next() 166 if err != nil { 167 if errors.Is(err, io.EOF) { 168 err = openAndWait() 169 if err != nil { 170 return err 171 } 172 tr = tar.NewReader(newSkipReader(r)) 173 continue 174 } 175 return err 176 } 177 break 178 } 179 tw := tar.NewWriter(w) 180 if err := tw.WriteHeader(hdr); err != nil { 181 return err 182 } 183 if _, err := io.Copy(tw, tr); err != nil { 184 return err 185 } 186 if err := copyTar(tw, tr); err != nil { 187 return err 188 } 189 return tw.Close() 190 } 191 192 type skipReader struct { 193 buf *bytes.Buffer 194 r io.Reader 195 } 196 197 func newSkipReader(r io.Reader) *skipReader { 198 return &skipReader{r: r} 199 } 200 201 func (sr *skipReader) Read(data []byte) (int, error) { 202 if sr.buf == nil { 203 if err := sr.skipZeroBlocks(); err != nil { 204 return 0, err 205 } 206 } 207 bufN, _ := sr.buf.Read(data) 208 if bufN == len(data) { 209 return bufN, nil 210 } 211 n, err := sr.r.Read(data[bufN:]) 212 return bufN + n, err 213 } 214 215 func (sr *skipReader) skipZeroBlocks() error { 216 sr.buf = &bytes.Buffer{} 217 zeroBlock := make([]byte, 512) 218 for { 219 _, err := io.CopyN(sr.buf, sr.r, 512) 220 if err != nil { 221 return err 222 } 223 if !bytes.Equal(sr.buf.Bytes(), zeroBlock) { 224 return nil 225 } 226 sr.buf.Reset() 227 } 228 } 229 230 // TODO: Refactor this into tarutil. 231 func copyTar(tw *tar.Writer, tr *tar.Reader) error { 232 for { 233 hdr, err := tr.Next() 234 if err != nil { 235 if errors.Is(err, io.EOF) { 236 return nil 237 } 238 return err 239 } 240 if err := tw.WriteHeader(hdr); err != nil { 241 return err 242 } 243 _, err = io.Copy(tw, tr) 244 if err != nil { 245 return err 246 } 247 } 248 } 249 250 func withSpoutCommit(ctx context.Context, pachClient *client.APIClient, pipelineInfo *pps.PipelineInfo, logger logs.TaggedLogger, f *os.File, cb func(*pfs.Commit, *tar.Reader) error) error { 251 repo := pipelineInfo.Pipeline.Name 252 return backoff.RetryUntilCancel(ctx, func() (retErr error) { 253 commit, err := pachClient.PfsAPIClient.StartCommit(ctx, &pfs.StartCommitRequest{ 254 Parent: client.NewCommit(repo, ""), 255 Branch: pipelineInfo.OutputBranch, 256 }) 257 if err != nil { 258 return err 259 } 260 defer func() { 261 if retErr != nil { 262 pachClient.DeleteCommit(repo, commit.ID) 263 return 264 } 265 if err := pachClient.FinishCommit(repo, commit.ID); retErr == nil { 266 retErr = err 267 } 268 }() 269 _, err = f.Seek(0, 0) 270 if err != nil { 271 return err 272 } 273 return cb(commit, tar.NewReader(f)) 274 }, backoff.NewInfiniteBackOff(), func(err error, d time.Duration) error { 275 logger.Logf("error in withSpoutCommit: %+v, retrying in: %+v", err, d) 276 return nil 277 }) 278 }