golang.org/x/build@v0.0.0-20240506185731-218518f32b70/maintner/netsource.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package maintner
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"log"
    17  	"net"
    18  	"net/http"
    19  	"net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/golang/protobuf/proto"
    27  	"golang.org/x/build/maintner/internal/robustio"
    28  	"golang.org/x/build/maintner/maintpb"
    29  	"golang.org/x/build/maintner/reclog"
    30  )
    31  
    32  // NewNetworkMutationSource returns a mutation source from a master server.
    33  // The server argument should be a URL to the JSON logs index.
    34  func NewNetworkMutationSource(server, cacheDir string) MutationSource {
    35  	base, err := url.Parse(server)
    36  	if err != nil {
    37  		panic(fmt.Sprintf("invalid URL: %q", server))
    38  	}
    39  	return &netMutSource{
    40  		server:   server,
    41  		base:     base,
    42  		cacheDir: cacheDir,
    43  	}
    44  }
    45  
    46  // TailNetworkMutationSource calls fn for all new mutations added to the log on server.
    47  // Events with the End field set to true are not sent, so all events will
    48  // have exactly one of Mutation or Err fields set to a non-zero value.
    49  // It ignores prior events.
    50  // If the server is restarted and its history diverges,
    51  // TailNetworkMutationSource may return duplicate events. This therefore does not
    52  // return a MutationSource, so it can't be accidentally misused for important things.
    53  // TailNetworkMutationSource returns if fn returns an error, if ctx expires,
    54  // or if it runs into a network error.
    55  func TailNetworkMutationSource(ctx context.Context, server string, fn func(MutationStreamEvent) error) error {
    56  	td, err := os.MkdirTemp("", "maintnertail")
    57  	if err != nil {
    58  		return err
    59  	}
    60  	defer robustio.RemoveAll(td)
    61  
    62  	ns := NewNetworkMutationSource(server, td).(*netMutSource)
    63  	ns.quiet = true
    64  	getSegs := func(waitSizeNot int64) ([]LogSegmentJSON, error) {
    65  		for {
    66  			segs, err := ns.getServerSegments(ctx, waitSizeNot)
    67  			if err != nil {
    68  				if err := ctx.Err(); err != nil {
    69  					return nil, err
    70  				}
    71  				// Sleep a minimum of 5 seconds before trying
    72  				// again. The user's fn function might sleep
    73  				// longer or shorter.
    74  				timer := time.NewTimer(5 * time.Second)
    75  				err := fn(MutationStreamEvent{Err: err})
    76  				if err != nil {
    77  					timer.Stop()
    78  					return nil, err
    79  				}
    80  				<-timer.C
    81  				continue
    82  			}
    83  			return segs, nil
    84  		}
    85  	}
    86  
    87  	// See how long the log is at start. Then we'll only fetch
    88  	// things after that.
    89  	segs, err := getSegs(0)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	segSize := sumJSONSegSize(segs)
    94  	lastSeg := segs[len(segs)-1]
    95  	if _, _, err := ns.syncSeg(ctx, lastSeg); err != nil {
    96  		return err
    97  	}
    98  
    99  	ticker := time.NewTicker(time.Second) // max re-fetch interval
   100  	defer ticker.Stop()
   101  	for {
   102  		segs, err := getSegs(segSize)
   103  		if err != nil {
   104  			return err
   105  		}
   106  		segSize = sumJSONSegSize(segs)
   107  
   108  		for _, seg := range segs {
   109  			if seg.Number < lastSeg.Number {
   110  				continue
   111  			}
   112  			var off int64
   113  			if seg.Number == lastSeg.Number {
   114  				off = lastSeg.Size
   115  			}
   116  			_, newData, err := ns.syncSeg(ctx, seg)
   117  			if err != nil {
   118  				return err
   119  			}
   120  			if err := reclog.ForeachRecord(bytes.NewReader(newData), off, func(off int64, hdr, rec []byte) error {
   121  				m := new(maintpb.Mutation)
   122  				if err := proto.Unmarshal(rec, m); err != nil {
   123  					return err
   124  				}
   125  				return fn(MutationStreamEvent{Mutation: m})
   126  			}); err != nil {
   127  				return err
   128  			}
   129  		}
   130  		lastSeg = segs[len(segs)-1]
   131  
   132  		<-ticker.C
   133  	}
   134  }
   135  
   136  type netMutSource struct {
   137  	server   string
   138  	base     *url.URL
   139  	cacheDir string
   140  
   141  	last  []fileSeg
   142  	quiet bool // disable verbose logging
   143  
   144  	// Hooks for testing. If nil, unused:
   145  	testHookGetServerSegments func(context.Context, int64) ([]LogSegmentJSON, error)
   146  	testHookSyncSeg           func(context.Context, LogSegmentJSON) (fileSeg, []byte, error)
   147  	testHookOnSplit           func(sumCommon int64)
   148  	testHookFilePrefixSum224  func(file string, n int64) string
   149  }
   150  
   151  func (ns *netMutSource) GetMutations(ctx context.Context) <-chan MutationStreamEvent {
   152  	ch := make(chan MutationStreamEvent, 50)
   153  	go func() {
   154  		err := ns.fetchAndSendMutations(ctx, ch)
   155  		final := MutationStreamEvent{Err: err}
   156  		if err == nil {
   157  			final.End = true
   158  		}
   159  		select {
   160  		case ch <- final:
   161  		case <-ctx.Done():
   162  		}
   163  	}()
   164  	return ch
   165  }
   166  
   167  // isNoInternetError reports whether the provided error is because there's no
   168  // network connectivity.
   169  func isNoInternetError(err error) bool {
   170  	if err == nil {
   171  		return false
   172  	}
   173  	switch err := err.(type) {
   174  	case fetchError:
   175  		return isNoInternetError(err.Err)
   176  	case *url.Error:
   177  		return isNoInternetError(err.Err)
   178  	case *net.OpError:
   179  		return isNoInternetError(err.Err)
   180  	case *net.DNSError:
   181  		// Trashy:
   182  		return err.Err == "no such host"
   183  	default:
   184  		log.Printf("Unknown error type %T: %#v", err, err)
   185  		return false
   186  	}
   187  }
   188  
   189  func (ns *netMutSource) locallyCachedSegments() (segs []fileSeg, err error) {
   190  	defer func() {
   191  		if err != nil {
   192  			log.Printf("No network connection and failed to use local cache: %v", err)
   193  		} else {
   194  			log.Printf("No network connection; using %d locally cached segments.", len(segs))
   195  		}
   196  	}()
   197  	des, err := os.ReadDir(ns.cacheDir)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	fiMap := map[string]fs.FileInfo{}
   202  	segHex := map[int]string{}
   203  	segGrowing := map[int]bool{}
   204  	for _, de := range des {
   205  		name := de.Name()
   206  		if !strings.HasSuffix(name, ".mutlog") {
   207  			continue
   208  		}
   209  		fi, err := de.Info()
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  		fiMap[name] = fi
   214  
   215  		if len(name) == len("0000.6897fab4d3afcda332424b2a2a1a4469021074282bc7be5606aaa221.mutlog") {
   216  			num, err := strconv.Atoi(name[:4])
   217  			if err != nil {
   218  				continue
   219  			}
   220  			segHex[num] = strings.TrimSuffix(name[5:], ".mutlog")
   221  		} else if strings.HasSuffix(name, ".growing.mutlog") {
   222  			num, err := strconv.Atoi(name[:4])
   223  			if err != nil {
   224  				continue
   225  			}
   226  			segGrowing[num] = true
   227  		}
   228  	}
   229  	for num := 0; ; num++ {
   230  		if hex, ok := segHex[num]; ok {
   231  			name := fmt.Sprintf("%04d.%s.mutlog", num, hex)
   232  			segs = append(segs, fileSeg{
   233  				seg:    num,
   234  				file:   filepath.Join(ns.cacheDir, name),
   235  				size:   fiMap[name].Size(),
   236  				sha224: hex,
   237  			})
   238  			continue
   239  		}
   240  		if segGrowing[num] {
   241  			name := fmt.Sprintf("%04d.growing.mutlog", num)
   242  			slurp, err := robustio.ReadFile(filepath.Join(ns.cacheDir, name))
   243  			if err != nil {
   244  				return nil, err
   245  			}
   246  			segs = append(segs, fileSeg{
   247  				seg:    num,
   248  				file:   filepath.Join(ns.cacheDir, name),
   249  				size:   int64(len(slurp)),
   250  				sha224: fmt.Sprintf("%x", sha256.Sum224(slurp)),
   251  			})
   252  		}
   253  		return segs, nil
   254  	}
   255  }
   256  
   257  // getServerSegments fetches the JSON logs handler (ns.server, usually
   258  // https://maintner.golang.org/logs) and returns the parsed JSON.
   259  // It sends the "waitsizenot" URL parameter, which specifies that the
   260  // request should long-poll waiting for the server to have a sum of
   261  // log segment sizes different than the value specified. As a result,
   262  // it blocks until the server has new data to send or ctx expires.
   263  //
   264  // getServerSegments returns an error that matches fetchError with
   265  // PossiblyRetryable set to true when it has signal that repeating
   266  // the same call after some time may succeed.
   267  func (ns *netMutSource) getServerSegments(ctx context.Context, waitSizeNot int64) ([]LogSegmentJSON, error) {
   268  	if fn := ns.testHookGetServerSegments; fn != nil {
   269  		return fn(ctx, waitSizeNot)
   270  	}
   271  	logsURL := fmt.Sprintf("%s?waitsizenot=%d", ns.server, waitSizeNot)
   272  	for {
   273  		req, err := http.NewRequestWithContext(ctx, "GET", logsURL, nil)
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  		res, err := http.DefaultClient.Do(req)
   278  		if err != nil {
   279  			return nil, fetchError{Err: err, PossiblyRetryable: true}
   280  		}
   281  		// When we're doing a long poll and the server replies
   282  		// with a 304 response, that means the server is just
   283  		// heart-beating us and trying to get a response back
   284  		// within its various deadlines. But we should just
   285  		// try again.
   286  		if res.StatusCode == http.StatusNotModified {
   287  			res.Body.Close()
   288  			continue
   289  		}
   290  		defer res.Body.Close()
   291  		if res.StatusCode/100 == 5 {
   292  			// Consider a 5xx server response to possibly succeed later.
   293  			return nil, fetchError{Err: fmt.Errorf("%s: %v", ns.server, res.Status), PossiblyRetryable: true}
   294  		} else if res.StatusCode != http.StatusOK {
   295  			return nil, fmt.Errorf("%s: %v", ns.server, res.Status)
   296  		}
   297  		b, err := io.ReadAll(res.Body)
   298  		if err != nil {
   299  			return nil, fetchError{Err: err, PossiblyRetryable: true}
   300  		}
   301  		var segs []LogSegmentJSON
   302  		err = json.Unmarshal(b, &segs)
   303  		if err != nil {
   304  			return nil, fmt.Errorf("unmarshaling %s JSON: %v", ns.server, err)
   305  		}
   306  		return segs, nil
   307  	}
   308  }
   309  
   310  // getNewSegments fetches new mutations from the network mutation source.
   311  // It tries to absorb the expected network bumps by trying multiple times,
   312  // and returns an error only when it considers the problem to be terminal.
   313  //
   314  // If there's no internet connectivity from the start, it returns locally
   315  // cached segments that might be available from before. Otherwise it waits
   316  // for internet connectivity to come back and keeps going when it does.
   317  func (ns *netMutSource) getNewSegments(ctx context.Context) ([]fileSeg, error) {
   318  	sumLast := sumSegSize(ns.last)
   319  
   320  	// First, fetch JSON metadata for the segments from the server.
   321  	var serverSegs []LogSegmentJSON
   322  	for try := 1; ; {
   323  		segs, err := ns.getServerSegments(ctx, sumLast)
   324  		if isNoInternetError(err) {
   325  			if sumLast == 0 {
   326  				return ns.locallyCachedSegments()
   327  			}
   328  			log.Printf("No internet; blocking.")
   329  			select {
   330  			case <-ctx.Done():
   331  				return nil, ctx.Err()
   332  			case <-time.After(15 * time.Second):
   333  				try = 1
   334  				continue
   335  			}
   336  		} else if fe := (fetchError{}); errors.As(err, &fe) && fe.PossiblyRetryable {
   337  			// Fetching the JSON logs handler happens over an unreliable network connection,
   338  			// and will fail at some point. Prefer to try again over reporting a terminal error.
   339  			const maxTries = 5
   340  			if try == maxTries {
   341  				// At this point, promote it to a terminal error.
   342  				return nil, fmt.Errorf("after %d attempts, fetching server segments still failed: %v", maxTries, err)
   343  			}
   344  			someDelay := time.Duration(try*try) * time.Second
   345  			log.Printf("fetching server segments did not succeed on attempt %d, will try again in %v: %v", try, someDelay, err)
   346  			select {
   347  			case <-ctx.Done():
   348  				return nil, ctx.Err()
   349  			case <-time.After(someDelay):
   350  				try++
   351  				continue
   352  			}
   353  		} else if err != nil {
   354  			return nil, err
   355  		}
   356  		serverSegs = segs
   357  		break
   358  	}
   359  	// TODO: optimization: if already on GCE, skip sync to disk part and just
   360  	// read from network. fast & free network inside.
   361  
   362  	// Second, fetch the new segments or their fragments
   363  	// that we don't yet have locally.
   364  	var fileSegs []fileSeg
   365  	for _, seg := range serverSegs {
   366  		for try := 1; ; {
   367  			fileSeg, _, err := ns.syncSeg(ctx, seg)
   368  			if isNoInternetError(err) {
   369  				log.Printf("No internet; blocking.")
   370  				select {
   371  				case <-ctx.Done():
   372  					return nil, ctx.Err()
   373  				case <-time.After(15 * time.Second):
   374  					try = 1
   375  					continue
   376  				}
   377  			} else if fe := (fetchError{}); errors.As(err, &fe) && fe.PossiblyRetryable {
   378  				// Syncing a segment fetches a good deal of data over a network connection,
   379  				// and will fail at some point. Be very willing to try again at this layer,
   380  				// since it's much more efficient than having GetMutations return an error
   381  				// and possibly cause a higher level retry to redo significantly more work.
   382  				const maxTries = 10
   383  				if try == maxTries {
   384  					// At this point, promote it to a terminal error.
   385  					return nil, fmt.Errorf("after %d attempts, syncing segment %d still failed: %v", maxTries, seg.Number, err)
   386  				}
   387  				someDelay := time.Duration(try*try) * time.Second
   388  				log.Printf("syncing segment %d did not succeed on attempt %d, will try again in %v: %v", seg.Number, try, someDelay, err)
   389  				select {
   390  				case <-ctx.Done():
   391  					return nil, ctx.Err()
   392  				case <-time.After(someDelay):
   393  					try++
   394  					continue
   395  				}
   396  			} else if err != nil {
   397  				return nil, err
   398  			}
   399  			fileSegs = append(fileSegs, fileSeg)
   400  			break
   401  		}
   402  	}
   403  
   404  	// Verify consistency of newly fetched data,
   405  	// and check there is in fact something new.
   406  	sumCommon := ns.sumCommonPrefixSize(fileSegs, ns.last)
   407  	if sumCommon != sumLast {
   408  		if fn := ns.testHookOnSplit; fn != nil {
   409  			fn(sumCommon)
   410  		}
   411  		// Our history diverged from the source.
   412  		return nil, ErrSplit
   413  	} else if sumCur := sumSegSize(fileSegs); sumCommon == sumCur {
   414  		// Nothing new. This shouldn't happen since the maintnerd server is required to handle
   415  		// the "?waitsizenot=NNN" long polling parameter, so it's a problem if we get here.
   416  		return nil, fmt.Errorf("maintner.netsource: maintnerd server returned unchanged log segments")
   417  	}
   418  	ns.last = fileSegs
   419  
   420  	newSegs := trimLeadingSegBytes(fileSegs, sumCommon)
   421  	return newSegs, nil
   422  }
   423  
   424  func trimLeadingSegBytes(in []fileSeg, trim int64) []fileSeg {
   425  	// First trim off whole segments, sharing the same underlying memory.
   426  	for len(in) > 0 && trim >= in[0].size {
   427  		trim -= in[0].size
   428  		in = in[1:]
   429  	}
   430  	if len(in) == 0 {
   431  		return nil
   432  	}
   433  	// Now copy, since we'll be modifying the first element.
   434  	out := append([]fileSeg(nil), in...)
   435  	out[0].skip = trim
   436  	return out
   437  }
   438  
   439  // filePrefixSum224 returns the lowercase hex SHA-224 of the first n bytes of file.
   440  func (ns *netMutSource) filePrefixSum224(file string, n int64) string {
   441  	if fn := ns.testHookFilePrefixSum224; fn != nil {
   442  		return fn(file, n)
   443  	}
   444  	f, err := os.Open(file)
   445  	if err != nil {
   446  		if !os.IsNotExist(err) {
   447  			log.Print(err)
   448  		}
   449  		return ""
   450  	}
   451  	defer f.Close()
   452  	h := sha256.New224()
   453  	_, err = io.CopyN(h, f, n)
   454  	if err != nil {
   455  		log.Print(err)
   456  		return ""
   457  	}
   458  	return fmt.Sprintf("%x", h.Sum(nil))
   459  }
   460  
   461  func sumSegSize(segs []fileSeg) (sum int64) {
   462  	for _, seg := range segs {
   463  		sum += seg.size
   464  	}
   465  	return
   466  }
   467  
   468  func sumJSONSegSize(segs []LogSegmentJSON) (sum int64) {
   469  	for _, seg := range segs {
   470  		sum += seg.Size
   471  	}
   472  	return
   473  }
   474  
   475  // sumCommonPrefixSize computes the size of the longest common prefix of file segments a and b
   476  // that can be found quickly by checking for matching checksums between segment boundaries.
   477  func (ns *netMutSource) sumCommonPrefixSize(a, b []fileSeg) (sum int64) {
   478  	for len(a) > 0 && len(b) > 0 {
   479  		sa, sb := a[0], b[0]
   480  		if sa.sha224 == sb.sha224 {
   481  			// Whole chunk in common.
   482  			sum += sa.size
   483  			a, b = a[1:], b[1:]
   484  			continue
   485  		}
   486  		if sa.size == sb.size {
   487  			// If they're the same size but different
   488  			// sums, it must've forked.
   489  			return
   490  		}
   491  		// See if one chunk is a prefix of the other.
   492  		// Make sa be the smaller one.
   493  		if sb.size < sa.size {
   494  			sa, sb = sb, sa
   495  		}
   496  		// Hash the beginning of the bigger size.
   497  		bPrefixSum := ns.filePrefixSum224(sb.file, sa.size)
   498  		if bPrefixSum == sa.sha224 {
   499  			sum += sa.size
   500  		}
   501  		break
   502  	}
   503  	return
   504  }
   505  
   506  // fetchAndSendMutations fetches new mutations from the network mutation source
   507  // and sends them to ch.
   508  func (ns *netMutSource) fetchAndSendMutations(ctx context.Context, ch chan<- MutationStreamEvent) error {
   509  	newSegs, err := ns.getNewSegments(ctx)
   510  	if err != nil {
   511  		return err
   512  	}
   513  	return foreachFileSeg(newSegs, func(seg fileSeg) error {
   514  		f, err := os.Open(seg.file)
   515  		if err != nil {
   516  			return err
   517  		}
   518  		defer f.Close()
   519  		if seg.skip > 0 {
   520  			if _, err := f.Seek(seg.skip, io.SeekStart); err != nil {
   521  				return err
   522  			}
   523  		}
   524  		return reclog.ForeachRecord(io.LimitReader(f, seg.size-seg.skip), seg.skip, func(off int64, hdr, rec []byte) error {
   525  			m := new(maintpb.Mutation)
   526  			if err := proto.Unmarshal(rec, m); err != nil {
   527  				return err
   528  			}
   529  			select {
   530  			case ch <- MutationStreamEvent{Mutation: m}:
   531  				return nil
   532  			case <-ctx.Done():
   533  				return ctx.Err()
   534  			}
   535  		})
   536  	})
   537  }
   538  
   539  func foreachFileSeg(segs []fileSeg, fn func(seg fileSeg) error) error {
   540  	for _, seg := range segs {
   541  		if err := fn(seg); err != nil {
   542  			return err
   543  		}
   544  	}
   545  	return nil
   546  }
   547  
   548  // TODO: add a constructor for this? or simplify it. make it Size +
   549  // File + embedded LogSegmentJSON?
   550  type fileSeg struct {
   551  	seg    int
   552  	file   string // full path
   553  	sha224 string
   554  	skip   int64
   555  	size   int64
   556  }
   557  
   558  // syncSeg syncs the provided log segment, returning its on-disk metadata.
   559  // The newData result is the new data that was added to the segment in this sync.
   560  //
   561  // syncSeg returns an error that matches fetchError with PossiblyRetryable set
   562  // to true when it has signal that repeating the same call after some time may
   563  // succeed.
   564  func (ns *netMutSource) syncSeg(ctx context.Context, seg LogSegmentJSON) (_ fileSeg, newData []byte, _ error) {
   565  	if fn := ns.testHookSyncSeg; fn != nil {
   566  		return fn(ctx, seg)
   567  	}
   568  
   569  	isFinalSeg := !strings.HasPrefix(seg.URL, "https://storage.googleapis.com/")
   570  	relURL, err := url.Parse(seg.URL)
   571  	if err != nil {
   572  		return fileSeg{}, nil, err
   573  	}
   574  	segURL := ns.base.ResolveReference(relURL)
   575  
   576  	frozen := filepath.Join(ns.cacheDir, fmt.Sprintf("%04d.%s.mutlog", seg.Number, seg.SHA224))
   577  
   578  	// Do we already have it? Files named in their final form with the sha224 are considered
   579  	// complete and immutable.
   580  	if fi, err := os.Stat(frozen); err == nil && fi.Size() == seg.Size {
   581  		return fileSeg{seg: seg.Number, file: frozen, size: fi.Size(), sha224: seg.SHA224}, nil, nil
   582  	}
   583  
   584  	// See how much data we already have in the partial growing file.
   585  	partial := filepath.Join(ns.cacheDir, fmt.Sprintf("%04d.growing.mutlog", seg.Number))
   586  	have, _ := robustio.ReadFile(partial)
   587  	if int64(len(have)) == seg.Size {
   588  		got224 := fmt.Sprintf("%x", sha256.Sum224(have))
   589  		if got224 == seg.SHA224 {
   590  			if !isFinalSeg {
   591  				// This was growing for us, but the server started a new growing segment.
   592  				if err := robustio.Rename(partial, frozen); err != nil {
   593  					return fileSeg{}, nil, err
   594  				}
   595  				return fileSeg{seg: seg.Number, file: frozen, sha224: seg.SHA224, size: seg.Size}, nil, nil
   596  			}
   597  			return fileSeg{seg: seg.Number, file: partial, sha224: seg.SHA224, size: seg.Size}, nil, nil
   598  		}
   599  	}
   600  
   601  	// Otherwise, download new data.
   602  	if int64(len(have)) < seg.Size {
   603  		req, err := http.NewRequestWithContext(ctx, "GET", segURL.String(), nil)
   604  		if err != nil {
   605  			return fileSeg{}, nil, err
   606  		}
   607  		req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", len(have), seg.Size-1))
   608  
   609  		if !ns.quiet {
   610  			log.Printf("Downloading %d bytes of %s ...", seg.Size-int64(len(have)), segURL)
   611  		}
   612  		res, err := http.DefaultClient.Do(req)
   613  		if err != nil {
   614  			return fileSeg{}, nil, fetchError{Err: err, PossiblyRetryable: true}
   615  		}
   616  		defer res.Body.Close()
   617  		if res.StatusCode/100 == 5 {
   618  			// Consider a 5xx server response to possibly succeed later.
   619  			return fileSeg{}, nil, fetchError{Err: fmt.Errorf("%s: %s", segURL.String(), res.Status), PossiblyRetryable: true}
   620  		} else if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusPartialContent {
   621  			return fileSeg{}, nil, fmt.Errorf("%s: %s", segURL.String(), res.Status)
   622  		}
   623  		newData, err = io.ReadAll(res.Body)
   624  		res.Body.Close()
   625  		if err != nil {
   626  			return fileSeg{}, nil, fetchError{Err: err, PossiblyRetryable: true}
   627  		}
   628  	}
   629  
   630  	// Commit to disk.
   631  	var newContents []byte
   632  	if int64(len(newData)) == seg.Size {
   633  		newContents = newData
   634  	} else if int64(len(have)+len(newData)) == seg.Size {
   635  		newContents = append(have, newData...)
   636  	} else if int64(len(have)) > seg.Size {
   637  		// We have more data than the server; likely because it restarted with uncommitted
   638  		// transactions, and so we're headed towards an ErrSplit. Reuse the longest common
   639  		// prefix as long as its checksum matches.
   640  		newContents = have[:seg.Size]
   641  	}
   642  	got224 := fmt.Sprintf("%x", sha256.Sum224(newContents))
   643  	if got224 != seg.SHA224 {
   644  		if len(have) == 0 {
   645  			return fileSeg{}, nil, errors.New("corrupt download")
   646  		}
   647  		// Try again.
   648  		os.Remove(partial)
   649  		return ns.syncSeg(ctx, seg)
   650  	}
   651  	// TODO: this is a quadratic amount of write I/O as the 16 MB
   652  	// segment grows. Switch to appending to the existing file,
   653  	// then perhaps encoding the desired file size into the
   654  	// filename suffix (instead of just *.growing.mutlog) so
   655  	// concurrent readers know where to stop.
   656  	tf, err := os.CreateTemp(ns.cacheDir, "tempseg")
   657  	if err != nil {
   658  		return fileSeg{}, nil, err
   659  	}
   660  	if _, err := tf.Write(newContents); err != nil {
   661  		return fileSeg{}, nil, err
   662  	}
   663  	if err := tf.Close(); err != nil {
   664  		return fileSeg{}, nil, err
   665  	}
   666  	finalName := partial
   667  	if !isFinalSeg {
   668  		finalName = frozen
   669  	}
   670  	if err := robustio.Rename(tf.Name(), finalName); err != nil {
   671  		return fileSeg{}, nil, err
   672  	}
   673  	if !ns.quiet {
   674  		log.Printf("wrote %v", finalName)
   675  	}
   676  	return fileSeg{seg: seg.Number, file: finalName, size: seg.Size, sha224: seg.SHA224}, newData, nil
   677  }
   678  
   679  type LogSegmentJSON struct {
   680  	Number int    `json:"number"`
   681  	Size   int64  `json:"size"`
   682  	SHA224 string `json:"sha224"`
   683  	URL    string `json:"url"`
   684  }
   685  
   686  // fetchError records an error during a fetch operation over an unreliable network.
   687  type fetchError struct {
   688  	Err error // Non-nil.
   689  
   690  	// PossiblyRetryable indicates whether Err is believed to be possibly caused by a
   691  	// non-terminal network error, such that the caller can expect it may not happen
   692  	// again if it simply tries the same fetch operation again after waiting a bit.
   693  	PossiblyRetryable bool
   694  }
   695  
   696  func (e fetchError) Error() string { return e.Err.Error() }
   697  func (e fetchError) Unwrap() error { return e.Err }