Bug 1506175 - Fetch recipes from Remote Settings r=mythmon,Gijs

Instead of obtaining the recipes from the Normandy server, obtain them from RemoteSettings

Differential Revision: https://phabricator.services.mozilla.com/D11490

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mathieu Leplatre 2018-12-11 16:10:21 +00:00
parent c46d35ae2f
commit 2bc67b2afd
3 changed files with 90 additions and 25 deletions

View File

@ -1742,6 +1742,7 @@ pref("app.normandy.first_run", true);
pref("app.normandy.logging.level", 50); // Warn
pref("app.normandy.run_interval_seconds", 21600); // 6 hours
pref("app.normandy.shieldLearnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield");
pref("app.normandy.remotesettings.enabled", false);
#ifdef MOZ_DATA_REPORTING
pref("app.shield.optoutstudies.enabled", true);
#else

View File

@ -13,6 +13,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "timerManager",
"nsIUpdateTimerManager");
XPCOMUtils.defineLazyModuleGetters(this, {
RemoteSettings: "resource://services-settings/remote-settings.js",
Storage: "resource://normandy/lib/Storage.jsm",
FilterExpressions: "resource://gre/modules/components-utils/FilterExpressions.jsm",
NormandyApi: "resource://normandy/lib/NormandyApi.jsm",
@ -26,6 +27,7 @@ var EXPORTED_SYMBOLS = ["RecipeRunner"];
const log = LogManager.getLogger("recipe-runner");
const TIMER_NAME = "recipe-client-addon-run";
const REMOTE_SETTINGS_COLLECTION = "normandy-recipes";
const PREF_CHANGED_TOPIC = "nsPref:changed";
const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
@ -37,6 +39,7 @@ const SHIELD_ENABLED_PREF = `${PREF_PREFIX}.enabled`;
const DEV_MODE_PREF = `${PREF_PREFIX}.dev_mode`;
const API_URL_PREF = `${PREF_PREFIX}.api_url`;
const LAZY_CLASSIFY_PREF = `${PREF_PREFIX}.experiments.lazy_classify`;
const REMOTE_SETTINGS_ENABLED_PREF = `${PREF_PREFIX}.remotesettings.enabled`;
const PREFS_TO_WATCH = [
RUN_INTERVAL_PREF,
@ -45,6 +48,12 @@ const PREFS_TO_WATCH = [
API_URL_PREF,
];
XPCOMUtils.defineLazyGetter(this, "gRemoteSettingsClient", () => {
return RemoteSettings(REMOTE_SETTINGS_COLLECTION, {
filterFunc: async recipe => RecipeRunner.checkFilter(recipe) ? recipe : null,
});
});
/**
* cacheProxy returns an object Proxy that will memoize properties of the target.
*/
@ -202,25 +211,11 @@ var RecipeRunner = {
}
// Fetch recipes before execution in case we fail and exit early.
let recipes;
let recipesToRun;
try {
recipes = await NormandyApi.fetchRecipes({enabled: true});
log.debug(
`Fetched ${recipes.length} recipes from the server: ` +
recipes.map(r => r.name).join(", ")
);
recipesToRun = await this.loadRecipes();
} catch (e) {
const apiUrl = Services.prefs.getCharPref(API_URL_PREF);
log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
let status = Uptake.RUNNER_SERVER_ERROR;
if (/NetworkError/.test(e)) {
status = Uptake.RUNNER_NETWORK_ERROR;
} else if (e instanceof NormandyApi.InvalidSignatureError) {
status = Uptake.RUNNER_INVALID_SIGNATURE;
}
Uptake.reportRunner(status);
// The legacy call to `Normandy.fetchRecipes()` can throw.
return;
}
@ -228,14 +223,6 @@ var RecipeRunner = {
await actions.fetchRemoteActions();
await actions.preExecution();
// Evaluate recipe filters
const recipesToRun = [];
for (const recipe of recipes) {
if (await this.checkFilter(recipe)) {
recipesToRun.push(recipe);
}
}
// Execute recipes, if we have any.
if (recipesToRun.length === 0) {
log.debug("No recipes to execute");
@ -250,6 +237,46 @@ var RecipeRunner = {
Uptake.reportRunner(Uptake.RUNNER_SUCCESS);
},
/**
* Return the list of recipes to run, filtered for the current environment.
*/
async loadRecipes() {
// If RemoteSettings is enabled, we read the list of recipes from there.
// The JEXL filtering is done via the provided callback.
if (Services.prefs.getBoolPref(REMOTE_SETTINGS_ENABLED_PREF, false)) {
return gRemoteSettingsClient.get();
}
// Obtain the recipes from the Normandy server (legacy).
let recipes;
try {
recipes = await NormandyApi.fetchRecipes({enabled: true});
log.debug(
`Fetched ${recipes.length} recipes from the server: ` +
recipes.map(r => r.name).join(", ")
);
} catch (e) {
const apiUrl = Services.prefs.getCharPref(API_URL_PREF);
log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
let status = Uptake.RUNNER_SERVER_ERROR;
if (/NetworkError/.test(e)) {
status = Uptake.RUNNER_NETWORK_ERROR;
} else if (e instanceof NormandyApi.InvalidSignatureError) {
status = Uptake.RUNNER_INVALID_SIGNATURE;
}
Uptake.reportRunner(status);
throw e;
}
// Evaluate recipe filters
const recipesToRun = [];
for (const recipe of recipes) {
if (await this.checkFilter(recipe)) {
recipesToRun.push(recipe);
}
}
return recipesToRun;
},
getFilterContext(recipe) {
const environment = cacheProxy(ClientEnvironment);
environment.recipe = {

View File

@ -10,6 +10,8 @@ ChromeUtils.import("resource://normandy/lib/ActionsManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
const { RemoteSettings } = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
add_task(async function getFilterContext() {
const recipe = {id: 17, arguments: {foo: "bar"}, unrelated: false};
const context = RecipeRunner.getFilterContext(recipe);
@ -171,6 +173,41 @@ decorate_task(
}
);
decorate_task(
withPrefEnv({
set: [
["app.normandy.remotesettings.enabled", true],
],
}),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(ActionsManager.prototype, "fetchRemoteActions"),
withStub(ActionsManager.prototype, "finalize"),
async function testReadFromRemoteSettings(
runRecipeStub,
fetchRemoteActionsStub,
finalizeStub,
) {
const matchRecipe = { id: "match", action: "matchAction", filter_expression: "true", _status: "synced", enabled: true };
const noMatchRecipe = { id: "noMatch", action: "noMatchAction", filter_expression: "false", _status: "synced", enabled: true };
const missingRecipe = { id: "missing", action: "missingAction", filter_expression: "true", _status: "synced", enabled: true };
const rsCollection = await RemoteSettings("normandy-recipes").openCollection();
await rsCollection.create(matchRecipe, { synced: true });
await rsCollection.create(noMatchRecipe, { synced: true });
await rsCollection.create(missingRecipe, { synced: true });
await rsCollection.db.saveLastModified(42);
rsCollection.db.close();
await RecipeRunner.run();
Assert.deepEqual(
runRecipeStub.args,
[[matchRecipe], [missingRecipe]],
"recipe with matching filters should be executed",
);
}
);
decorate_task(
withMockNormandyApi,
async function testRunFetchFail(mockApi) {