doctor.sys.mjs (7033B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // A doctor for our collections. She can be asked to make a consultation, and 6 // may just diagnose an issue without attempting to cure it, may diagnose and 7 // attempt to cure, or may decide she is overworked and underpaid. 8 // Or something - naming is hard :) 9 10 import { Log } from "resource://gre/modules/Log.sys.mjs"; 11 12 import { Async } from "resource://services-common/async.sys.mjs"; 13 import { Observers } from "resource://services-common/observers.sys.mjs"; 14 import { Service } from "resource://services-sync/service.sys.mjs"; 15 import { Resource } from "resource://services-sync/resource.sys.mjs"; 16 import { Svc } from "resource://services-sync/util.sys.mjs"; 17 18 const log = Log.repository.getLogger("Sync.Doctor"); 19 20 export var Doctor = { 21 async consult(recentlySyncedEngines) { 22 if (!Services.telemetry.canRecordBase) { 23 log.info("Skipping consultation: telemetry reporting is disabled"); 24 return; 25 } 26 27 let engineInfos = this._getEnginesToValidate(recentlySyncedEngines); 28 29 await this._runValidators(engineInfos); 30 }, 31 32 _getEnginesToValidate(recentlySyncedEngines) { 33 let result = {}; 34 for (let e of recentlySyncedEngines) { 35 let prefPrefix = `engine.${e.name}.`; 36 if ( 37 !Svc.PrefBranch.getBoolPref(prefPrefix + "validation.enabled", false) 38 ) { 39 log.info(`Skipping check of ${e.name} - disabled via preferences`); 40 continue; 41 } 42 // Check the last validation time for the engine. 43 let lastValidation = Svc.PrefBranch.getIntPref( 44 prefPrefix + "validation.lastTime", 45 0 46 ); 47 let validationInterval = Svc.PrefBranch.getIntPref( 48 prefPrefix + "validation.interval" 49 ); 50 let nowSeconds = this._now(); 51 52 if (nowSeconds - lastValidation < validationInterval) { 53 log.info( 54 `Skipping validation of ${e.name}: too recent since last validation attempt` 55 ); 56 continue; 57 } 58 // Update the time now, even if we decline to actually perform a 59 // validation. We don't want to check the rest of these more frequently 60 // than once a day. 61 Svc.PrefBranch.setIntPref( 62 prefPrefix + "validation.lastTime", 63 Math.floor(nowSeconds) 64 ); 65 66 // Validation only occurs a certain percentage of the time. 67 let validationProbability = 68 Svc.PrefBranch.getIntPref( 69 prefPrefix + "validation.percentageChance", 70 0 71 ) / 100.0; 72 if (validationProbability < Math.random()) { 73 log.info( 74 `Skipping validation of ${e.name}: Probability threshold not met` 75 ); 76 continue; 77 } 78 79 let maxRecords = Svc.PrefBranch.getIntPref( 80 prefPrefix + "validation.maxRecords" 81 ); 82 if (!maxRecords) { 83 log.info(`Skipping validation of ${e.name}: No maxRecords specified`); 84 continue; 85 } 86 // OK, so this is a candidate - the final decision will be based on the 87 // number of records actually found. 88 result[e.name] = { engine: e, maxRecords }; 89 } 90 return result; 91 }, 92 93 async _runValidators(engineInfos) { 94 if (!Object.keys(engineInfos).length) { 95 log.info("Skipping validation: no engines qualify"); 96 return; 97 } 98 99 if (Object.values(engineInfos).filter(i => i.maxRecords != -1).length) { 100 // at least some of the engines have maxRecord restrictions which require 101 // us to ask the server for the counts. 102 let countInfo = await this._fetchCollectionCounts(); 103 for (let [engineName, recordCount] of Object.entries(countInfo)) { 104 if (engineName in engineInfos) { 105 engineInfos[engineName].recordCount = recordCount; 106 } 107 } 108 } 109 110 for (let [ 111 engineName, 112 { engine, maxRecords, recordCount }, 113 ] of Object.entries(engineInfos)) { 114 // maxRecords of -1 means "any number", so we can skip asking the server. 115 // Used for tests. 116 if (maxRecords >= 0 && recordCount > maxRecords) { 117 log.debug( 118 `Skipping validation for ${engineName} because ` + 119 `the number of records (${recordCount}) is greater ` + 120 `than the maximum allowed (${maxRecords}).` 121 ); 122 continue; 123 } 124 let validator = engine.getValidator(); 125 if (!validator) { 126 // This is probably only possible in profile downgrade cases. 127 log.warn( 128 `engine.getValidator returned null for ${engineName} but the pref that controls validation is enabled.` 129 ); 130 continue; 131 } 132 133 if (!(await validator.canValidate())) { 134 log.debug( 135 `Skipping validation for ${engineName} because validator.canValidate() is false` 136 ); 137 continue; 138 } 139 140 // Let's do it! 141 Services.console.logStringMessage( 142 `Sync is about to run a consistency check of ${engine.name}. This may be slow, and ` + 143 `can be controlled using the pref "services.sync.${engine.name}.validation.enabled".\n` + 144 `If you encounter any problems because of this, please file a bug.` 145 ); 146 147 try { 148 log.info(`Running validator for ${engine.name}`); 149 let result = await validator.validate(engine); 150 let { problems, version, duration, recordCount } = result; 151 Observers.notify( 152 "weave:engine:validate:finish", 153 { 154 version, 155 checked: recordCount, 156 took: duration, 157 problems: problems ? problems.getSummary(true) : null, 158 }, 159 engine.name 160 ); 161 } catch (ex) { 162 if (Async.isShutdownException(ex)) { 163 throw ex; 164 } 165 log.error(`Failed to run validation on ${engine.name}!`, ex); 166 Observers.notify("weave:engine:validate:error", ex, engine.name); 167 // Keep validating -- there's no reason to think that a failure for one 168 // validator would mean the others will fail. 169 } 170 } 171 }, 172 173 // mainly for mocking. 174 async _fetchCollectionCounts() { 175 let collectionCountsURL = Service.userBaseURL + "info/collection_counts"; 176 try { 177 let infoResp = await Service._fetchInfo(collectionCountsURL); 178 if (!infoResp.success) { 179 log.error( 180 "Can't fetch collection counts: request to info/collection_counts responded with " + 181 infoResp.status 182 ); 183 return {}; 184 } 185 return infoResp.obj; // might throw because obj is a getter which parses json. 186 } catch (ex) { 187 if (Async.isShutdownException(ex)) { 188 throw ex; 189 } 190 // Not running validation is totally fine, so we just write an error log and return. 191 log.error("Caught error when fetching counts", ex); 192 return {}; 193 } 194 }, 195 196 // functions used so tests can mock them 197 _now() { 198 // We use the server time, which is SECONDS 199 return Resource.serverTime; 200 }, 201 };