github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/reproxy/compare.go (about)

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package reproxy
    16  
    17  import (
    18  	"context"
    19  	"path/filepath"
    20  	"sort"
    21  
    22  	lpb "github.com/bazelbuild/reclient/api/log"
    23  )
    24  
    25  func compareAction(ctx context.Context, s *Server, a *action) {
    26  	if !a.compare {
    27  		return
    28  	}
    29  	mismatches := make(map[string]*lpb.Verification_Mismatch)
    30  	var localExitCodes []int32
    31  	var remoteExitCodes []int32
    32  	numVerified := 0
    33  
    34  	for _, rerun := range a.rec.LocalMetadata.RerunMetadata {
    35  		numVerified += findMismatches(rerun.OutputFileDigests, rerun.OutputDirectoryDigests, mismatches, a, false)
    36  		localExitCodes = append(localExitCodes, rerun.Result.ExitCode)
    37  	}
    38  
    39  	if a.rec.RemoteMetadata != nil {
    40  		for _, rerun := range a.rec.RemoteMetadata.RerunMetadata {
    41  			numVerified += findMismatches(rerun.OutputFileDigests, rerun.OutputDirectoryDigests, mismatches, a, true)
    42  			remoteExitCodes = append(remoteExitCodes, rerun.Result.ExitCode)
    43  		}
    44  	}
    45  
    46  	localExitCodes = dedupExitCodes(localExitCodes)
    47  	remoteExitCodes = dedupExitCodes(remoteExitCodes)
    48  
    49  	for fp, mismatch := range mismatches {
    50  		mismatch.LocalExitCodes = localExitCodes
    51  		mismatch.RemoteExitCodes = remoteExitCodes
    52  
    53  		dgStatus, shouldLogDg := compareDigests(mismatch.LocalDigests, mismatch.RemoteDigests, a.numLocalReruns, a.numRemoteReruns)
    54  		ecStatus, shouldLogEc := compareExitCodes(mismatch.LocalExitCodes, mismatch.RemoteExitCodes, a.numLocalReruns, a.numRemoteReruns)
    55  
    56  		// Delete the records that we don't want to keep.
    57  		if !shouldLogDg && !shouldLogEc {
    58  			delete(mismatches, fp)
    59  		} else if !shouldLogDg {
    60  			mismatch.LocalDigests = nil
    61  			mismatch.RemoteDigests = nil
    62  		} else if !shouldLogEc {
    63  			mismatch.LocalExitCodes = nil
    64  			mismatch.RemoteExitCodes = nil
    65  		}
    66  
    67  		switch {
    68  		case dgStatus == lpb.DeterminismStatus_UNKNOWN || ecStatus == lpb.DeterminismStatus_UNKNOWN:
    69  			mismatch.Determinism = lpb.DeterminismStatus_UNKNOWN
    70  		case dgStatus == lpb.DeterminismStatus_REMOTE_NON_DETERMINISTIC || ecStatus == lpb.DeterminismStatus_REMOTE_NON_DETERMINISTIC:
    71  			mismatch.Determinism = lpb.DeterminismStatus_REMOTE_NON_DETERMINISTIC
    72  		case dgStatus == lpb.DeterminismStatus_NON_DETERMINISTIC || ecStatus == lpb.DeterminismStatus_NON_DETERMINISTIC:
    73  			mismatch.Determinism = lpb.DeterminismStatus_NON_DETERMINISTIC
    74  		default:
    75  			mismatch.Determinism = lpb.DeterminismStatus_DETERMINISTIC
    76  		}
    77  	}
    78  	verRes := mismatchesToProto(mismatches, numVerified)
    79  	a.rec.LocalMetadata.Verification = verRes
    80  }
    81  
    82  func mismatchesToProto(mismatches map[string]*lpb.Verification_Mismatch, numVerified int) *lpb.Verification {
    83  	// We need to output mismatches in fixed order, for tests.
    84  	var keys []string
    85  	for n := range mismatches {
    86  		keys = append(keys, n)
    87  	}
    88  	sort.Strings(keys)
    89  	res := &lpb.Verification{}
    90  	for _, path := range keys {
    91  		res.Mismatches = append(res.Mismatches, mismatches[path])
    92  	}
    93  	res.TotalMismatches = int32(len(mismatches))
    94  	res.TotalVerified = int64(numVerified)
    95  	return res
    96  }
    97  
    98  // Returns the determinism status of the digests and whether we should log the mismatch.
    99  func compareDigests(localDigests []string, remoteDigests []string, numLocalReruns int, numRemoteReruns int) (lpb.DeterminismStatus, bool) {
   100  	totalReruns := numLocalReruns + numRemoteReruns
   101  	localMismatches := len(localDigests)
   102  	remoteMismatches := len(remoteDigests)
   103  
   104  	if localMismatches > 1 || remoteMismatches > 1 {
   105  		if localMismatches == 1 {
   106  			return lpb.DeterminismStatus_REMOTE_NON_DETERMINISTIC, true
   107  		}
   108  		return lpb.DeterminismStatus_NON_DETERMINISTIC, true
   109  	}
   110  
   111  	deterministicMismatch := localMismatches == 1 && remoteMismatches == 1 && localDigests[0] != remoteDigests[0] && totalReruns > 2
   112  	localDeterministic := localMismatches == 1 && numRemoteReruns == 0
   113  	remoteDeterministic := remoteMismatches == 1 && numLocalReruns == 0
   114  	deterministic := localMismatches == 1 && remoteMismatches == 1 && localDigests[0] == remoteDigests[0]
   115  
   116  	if localDeterministic || remoteDeterministic || deterministic || deterministicMismatch {
   117  		return lpb.DeterminismStatus_DETERMINISTIC, deterministicMismatch
   118  	}
   119  
   120  	return lpb.DeterminismStatus_UNKNOWN, true
   121  }
   122  
   123  // Returns the determinism status of the exit codes and whether we should log the mismatch.
   124  func compareExitCodes(localExitCodes []int32, remoteExitCodes []int32, numLocalReruns int, numRemoteReruns int) (lpb.DeterminismStatus, bool) {
   125  	totalReruns := numLocalReruns + numRemoteReruns
   126  	localMismatches := len(localExitCodes)
   127  	remoteMismatches := len(remoteExitCodes)
   128  
   129  	if localMismatches > 1 || remoteMismatches > 1 {
   130  		if localMismatches == 1 {
   131  			return lpb.DeterminismStatus_REMOTE_NON_DETERMINISTIC, true
   132  		}
   133  		return lpb.DeterminismStatus_NON_DETERMINISTIC, true
   134  	}
   135  
   136  	deterministicMismatch := localMismatches == 1 && remoteMismatches == 1 && localExitCodes[0] != remoteExitCodes[0] && totalReruns > 2
   137  	localDeterministic := localMismatches == 1 && numRemoteReruns == 0
   138  	remoteDeterministic := remoteMismatches == 1 && numLocalReruns == 0
   139  	deterministic := localMismatches == 1 && remoteMismatches == 1 && localExitCodes[0] == remoteExitCodes[0]
   140  
   141  	if localDeterministic || remoteDeterministic || deterministic || deterministicMismatch {
   142  		return lpb.DeterminismStatus_DETERMINISTIC, deterministicMismatch
   143  	}
   144  
   145  	return lpb.DeterminismStatus_UNKNOWN, true
   146  }
   147  
   148  func findMismatches(fileDg map[string]string, dirDg map[string]string, mismatches map[string]*lpb.Verification_Mismatch, a *action, remote bool) int {
   149  	numVerified := 0
   150  	allDg := make(map[string]string)
   151  	for k, v := range fileDg {
   152  		allDg[k] = v
   153  	}
   154  	for k, v := range dirDg {
   155  		allDg[k] = v
   156  	}
   157  
   158  	for fp, dg := range allDg {
   159  		// Paths need to be normalized to forward slashes as all RBE paths use forward slashes irrespective of platform.
   160  		fp = filepath.ToSlash(fp)
   161  		if _, ok := mismatches[fp]; !ok {
   162  			numVerified++
   163  			mismatches[fp] = &lpb.Verification_Mismatch{
   164  				Path:         fp,
   165  				ActionDigest: a.digest,
   166  			}
   167  		}
   168  		if remote {
   169  			mismatches[fp].RemoteDigests = dedup(append(mismatches[fp].RemoteDigests, dg))
   170  		} else {
   171  			mismatches[fp].LocalDigests = dedup(append(mismatches[fp].LocalDigests, dg))
   172  		}
   173  	}
   174  	return numVerified
   175  }
   176  
   177  func dedupExitCodes(list []int32) []int32 {
   178  	var res []int32
   179  	seen := make(map[int32]bool)
   180  
   181  	for _, n := range list {
   182  		if _, ok := seen[n]; !ok {
   183  			seen[n] = true
   184  			res = append(res, n)
   185  		}
   186  	}
   187  
   188  	return res
   189  }