github.com/release-engineering/exodus-rsync@v1.11.2/internal/cmd/mixed.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"io"
     7  	"os/exec"
     8  	"sync"
     9  
    10  	"github.com/release-engineering/exodus-rsync/internal/args"
    11  	"github.com/release-engineering/exodus-rsync/internal/conf"
    12  	"github.com/release-engineering/exodus-rsync/internal/log"
    13  	"github.com/release-engineering/exodus-rsync/internal/rsync"
    14  )
    15  
    16  // Mixed publish mode, publishing both via exodus and rsync.
    17  // If either one fails, it kills/cancels the other.
    18  func mixedMain(ctx context.Context, cfg conf.Config, args args.Config) int {
    19  	logger := log.FromContext(ctx)
    20  
    21  	rsyncCmd, err := ext.rsync.Command(ctx, rsync.Arguments(ctx, args))
    22  	if err != nil {
    23  		logger.F("error", err).Error("Failed to generate rsync command")
    24  		return 25
    25  	}
    26  
    27  	ctx, cancelFn := context.WithCancel(ctx)
    28  
    29  	wg := sync.WaitGroup{}
    30  	wg.Add(2)
    31  
    32  	defer wg.Wait()
    33  	defer cancelFn()
    34  
    35  	// If either one of the publishes fails, we call this function to bail out.
    36  	//
    37  	// The main point of this is to ensure that the most relevant log message appears
    38  	// last. If we just let cancel & return happen "naturally", then e.g. if rsync
    39  	// fails, the last few error messages will be about the cancellation of exodus publish
    40  	// and a non-expert reader will probably wrongly conclude that *this* was the problem
    41  	// and miss the error message relating to rsync.
    42  	bailOut := func(cancelMessage, errorMessage string, exitCode int) int {
    43  		logger.Warn(cancelMessage)
    44  
    45  		// If one of rsync/exodus is still in progress, cancel it.
    46  		cancelFn()
    47  
    48  		// Wait for both to complete/fail.
    49  		wg.Wait()
    50  
    51  		// Then log the message.
    52  		logger.Error(errorMessage)
    53  
    54  		return exitCode
    55  	}
    56  
    57  	var lastCode *chan int
    58  	rsyncCode := make(chan int, 1)
    59  	exodusCode := make(chan int, 1)
    60  
    61  	// Let rsync & exodus publishes run in their own goroutines.
    62  	go func() {
    63  		defer wg.Done()
    64  		exodusCode <- exodusMain(ctx, cfg, args)
    65  	}()
    66  
    67  	go func() {
    68  		defer wg.Done()
    69  		rsyncCode <- doRsyncCommand(ctx, rsyncCmd)
    70  	}()
    71  
    72  	select {
    73  	case code := <-exodusCode:
    74  		if code != 0 {
    75  			return bailOut(
    76  				"Cancelling rsync due to errors in exodus publish...",
    77  				"Publish via exodus-gw failed", code)
    78  		}
    79  		logger.Info("Finished exodus publish, waiting on rsync...")
    80  		lastCode = &rsyncCode
    81  	case code := <-rsyncCode:
    82  		if code != 0 {
    83  			return bailOut(
    84  				"Cancelling exodus publish due to errors in rsync...",
    85  				"Publish via rsync failed", code)
    86  		}
    87  		logger.Info("Finished rsync publish, waiting on exodus...")
    88  		lastCode = &exodusCode
    89  	}
    90  
    91  	return <-*lastCode
    92  }
    93  
    94  func doRsyncCommand(ctx context.Context, cmd *exec.Cmd) int {
    95  	logger := log.FromContext(ctx)
    96  
    97  	var outPipe, errPipe io.ReadCloser
    98  
    99  	outPipe, err := cmd.StdoutPipe()
   100  	if err == nil {
   101  		errPipe, err = cmd.StderrPipe()
   102  	}
   103  	if err != nil {
   104  		logger.F("error", err).Error("Can't connect pipes to rsync")
   105  		return 39
   106  	}
   107  
   108  	outScanner := bufio.NewScanner(outPipe)
   109  	errScanner := bufio.NewScanner(errPipe)
   110  
   111  	err = cmd.Start()
   112  	if err != nil {
   113  		logger.F("error", err).Error("Failed to run rsync")
   114  		return 25
   115  	}
   116  
   117  	pid := cmd.Process.Pid
   118  
   119  	wg := sync.WaitGroup{}
   120  	wg.Add(2)
   121  
   122  	entry := logger.F("rsync", pid)
   123  	type logFunc func(string)
   124  
   125  	piper := func(scanner *bufio.Scanner, log logFunc) {
   126  		defer wg.Done()
   127  		for {
   128  			if !scanner.Scan() {
   129  				return
   130  			}
   131  			log(scanner.Text())
   132  		}
   133  	}
   134  
   135  	go piper(outScanner, entry.Info)
   136  	go piper(errScanner, entry.Warn)
   137  	wg.Wait()
   138  
   139  	err = cmd.Wait()
   140  	if err != nil {
   141  		logger.F("error", err).Error("rsync failed")
   142  		return 130
   143  	}
   144  
   145  	return 0
   146  }