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 }