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  }