test_node_reassignment.js (15718B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 _( 5 "Test that node reassignment responses are respected on all kinds of " + 6 "requests." 7 ); 8 9 const { RESTRequest } = ChromeUtils.importESModule( 10 "resource://services-common/rest.sys.mjs" 11 ); 12 const { Service } = ChromeUtils.importESModule( 13 "resource://services-sync/service.sys.mjs" 14 ); 15 16 add_task(async function setup() { 17 validate_all_future_pings(); 18 }); 19 20 /** 21 * Emulate the following Zeus config: 22 * $draining = data.get($prefix . $host . " draining"); 23 * if ($draining == "drain.") { 24 * log.warn($log_host_db_status . " migrating=1 (node-reassignment)" . 25 * $log_suffix); 26 * http.sendResponse("401 Node reassignment", $content_type, 27 * '"server request: node reassignment"', ""); 28 * } 29 */ 30 const reassignBody = '"server request: node reassignment"'; 31 32 // API-compatible with SyncServer handler. Bind `handler` to something to use 33 // as a ServerCollection handler. 34 function handleReassign(handler, req, resp) { 35 resp.setStatusLine(req.httpVersion, 401, "Node reassignment"); 36 resp.setHeader("Content-Type", "application/json"); 37 resp.bodyOutputStream.write(reassignBody, reassignBody.length); 38 } 39 40 async function prepareServer() { 41 let server = new SyncServer(); 42 server.registerUser("johndoe"); 43 server.start(); 44 syncTestLogging(); 45 await configureIdentity({ username: "johndoe" }, server); 46 return server; 47 } 48 49 function getReassigned() { 50 try { 51 return Services.prefs.getBoolPref("services.sync.lastSyncReassigned"); 52 } catch (ex) { 53 if (ex.result != Cr.NS_ERROR_UNEXPECTED) { 54 do_throw( 55 "Got exception retrieving lastSyncReassigned: " + Log.exceptionStr(ex) 56 ); 57 } 58 } 59 return false; 60 } 61 62 /** 63 * Make a test request to `url`, then watch the result of two syncs 64 * to ensure that a node request was made. 65 * Runs `between` between the two. This can be used to undo deliberate failure 66 * setup, detach observers, etc. 67 */ 68 async function syncAndExpectNodeReassignment( 69 server, 70 firstNotification, 71 between, 72 secondNotification, 73 url 74 ) { 75 let deferred = Promise.withResolvers(); 76 77 let getTokenCount = 0; 78 let mockTSC = { 79 // TokenServerClient 80 async getTokenUsingOAuth() { 81 getTokenCount++; 82 return { endpoint: server.baseURI + "1.1/johndoe/" }; 83 }, 84 }; 85 Service.identity._tokenServerClient = mockTSC; 86 87 // Make sure that it works! 88 let request = new RESTRequest(url); 89 let response = await request.get(); 90 Assert.equal(response.status, 401); 91 92 function onFirstSync() { 93 _("First sync completed."); 94 Svc.Obs.remove(firstNotification, onFirstSync); 95 Svc.Obs.add(secondNotification, onSecondSync); 96 97 Assert.equal(Service.clusterURL, ""); 98 99 // Allow for tests to clean up error conditions. 100 between(); 101 } 102 function onSecondSync() { 103 _("Second sync completed."); 104 Svc.Obs.remove(secondNotification, onSecondSync); 105 Service.scheduler.clearSyncTriggers(); 106 107 // Make absolutely sure that any event listeners are done with their work 108 // before we proceed. 109 waitForZeroTimer(function () { 110 _("Second sync nextTick."); 111 Assert.equal(getTokenCount, 1); 112 Service.startOver().then(() => { 113 server.stop(deferred.resolve); 114 }); 115 }); 116 } 117 118 Svc.Obs.add(firstNotification, onFirstSync); 119 await Service.sync(); 120 121 await deferred.promise; 122 } 123 124 add_task(async function test_momentary_401_engine() { 125 enableValidationPrefs(); 126 127 _("Test a failure for engine URLs that's resolved by reassignment."); 128 let server = await prepareServer(); 129 let john = server.user("johndoe"); 130 131 _("Enabling the Rotary engine."); 132 let { engine, syncID, tracker } = await registerRotaryEngine(); 133 134 // We need the server to be correctly set up prior to experimenting. Do this 135 // through a sync. 136 let global = { 137 syncID: Service.syncID, 138 storageVersion: STORAGE_VERSION, 139 rotary: { version: engine.version, syncID }, 140 }; 141 john.createCollection("meta").insert("global", global); 142 143 _("First sync to prepare server contents."); 144 await Service.sync(); 145 146 let numResets = 0; 147 let observeReset = (obs, topic) => { 148 if (topic == "rotary") { 149 numResets += 1; 150 } 151 }; 152 _("Adding observer that we saw an engine reset."); 153 Svc.Obs.add("weave:engine:reset-client:finish", observeReset); 154 155 _("Setting up Rotary collection to 401."); 156 let rotary = john.createCollection("rotary"); 157 let oldHandler = rotary.collectionHandler; 158 rotary.collectionHandler = handleReassign.bind(this, undefined); 159 160 // We want to verify that the clusterURL pref has been cleared after a 401 161 // inside a sync. Flag the Rotary engine to need syncing. 162 john.collection("rotary").timestamp += 1000; 163 164 function between() { 165 _("Undoing test changes."); 166 rotary.collectionHandler = oldHandler; 167 168 function onLoginStart() { 169 // lastSyncReassigned shouldn't be cleared until a sync has succeeded. 170 _("Ensuring that lastSyncReassigned is still set at next sync start."); 171 Svc.Obs.remove("weave:service:login:start", onLoginStart); 172 Assert.ok(getReassigned()); 173 } 174 175 _("Adding observer that lastSyncReassigned is still set on login."); 176 Svc.Obs.add("weave:service:login:start", onLoginStart); 177 } 178 179 await syncAndExpectNodeReassignment( 180 server, 181 "weave:service:sync:finish", 182 between, 183 "weave:service:sync:finish", 184 Service.storageURL + "rotary" 185 ); 186 187 Svc.Obs.remove("weave:engine:reset-client:finish", observeReset); 188 Assert.equal(numResets, 1); 189 await tracker.clearChangedIDs(); 190 await Service.engineManager.unregister(engine); 191 }); 192 193 // This test ends up being a failing fetch *after we're already logged in*. 194 add_task(async function test_momentary_401_info_collections() { 195 enableValidationPrefs(); 196 197 _("Test a failure for info/collections that's resolved by reassignment."); 198 let server = await prepareServer(); 199 200 _("First sync to prepare server contents."); 201 await Service.sync(); 202 203 // Return a 401 for info requests, particularly info/collections. 204 let oldHandler = server.toplevelHandlers.info; 205 server.toplevelHandlers.info = handleReassign; 206 207 function undo() { 208 _("Undoing test changes."); 209 server.toplevelHandlers.info = oldHandler; 210 } 211 212 await syncAndExpectNodeReassignment( 213 server, 214 "weave:service:sync:error", 215 undo, 216 "weave:service:sync:finish", 217 Service.infoURL 218 ); 219 }); 220 221 add_task(async function test_momentary_401_storage_loggedin() { 222 enableValidationPrefs(); 223 224 _( 225 "Test a failure for any storage URL, not just engine parts. " + 226 "Resolved by reassignment." 227 ); 228 let server = await prepareServer(); 229 230 _("Performing initial sync to ensure we are logged in."); 231 await Service.sync(); 232 233 // Return a 401 for all storage requests. 234 let oldHandler = server.toplevelHandlers.storage; 235 server.toplevelHandlers.storage = handleReassign; 236 237 function undo() { 238 _("Undoing test changes."); 239 server.toplevelHandlers.storage = oldHandler; 240 } 241 242 Assert.ok(Service.isLoggedIn, "already logged in"); 243 await syncAndExpectNodeReassignment( 244 server, 245 "weave:service:sync:error", 246 undo, 247 "weave:service:sync:finish", 248 Service.storageURL + "meta/global" 249 ); 250 }); 251 252 add_task(async function test_momentary_401_storage_loggedout() { 253 enableValidationPrefs(); 254 255 _( 256 "Test a failure for any storage URL, not just engine parts. " + 257 "Resolved by reassignment." 258 ); 259 let server = await prepareServer(); 260 261 // Return a 401 for all storage requests. 262 let oldHandler = server.toplevelHandlers.storage; 263 server.toplevelHandlers.storage = handleReassign; 264 265 function undo() { 266 _("Undoing test changes."); 267 server.toplevelHandlers.storage = oldHandler; 268 } 269 270 Assert.ok(!Service.isLoggedIn, "not already logged in"); 271 await syncAndExpectNodeReassignment( 272 server, 273 "weave:service:login:error", 274 undo, 275 "weave:service:sync:finish", 276 Service.storageURL + "meta/global" 277 ); 278 }); 279 280 add_task(async function test_loop_avoidance_storage() { 281 enableValidationPrefs(); 282 283 _( 284 "Test that a repeated failure doesn't result in a sync loop " + 285 "if node reassignment cannot resolve the failure." 286 ); 287 288 let server = await prepareServer(); 289 290 // Return a 401 for all storage requests. 291 let oldHandler = server.toplevelHandlers.storage; 292 server.toplevelHandlers.storage = handleReassign; 293 294 let firstNotification = "weave:service:login:error"; 295 let secondNotification = "weave:service:login:error"; 296 let thirdNotification = "weave:service:sync:finish"; 297 298 let deferred = Promise.withResolvers(); 299 300 let getTokenCount = 0; 301 let mockTSC = { 302 // TokenServerClient 303 async getTokenUsingOAuth() { 304 getTokenCount++; 305 return { endpoint: server.baseURI + "1.1/johndoe/" }; 306 }, 307 }; 308 Service.identity._tokenServerClient = mockTSC; 309 310 // Track the time. We want to make sure the duration between the first and 311 // second sync is small, and then that the duration between second and third 312 // is set to be large. 313 let now; 314 315 function onFirstSync() { 316 _("First sync completed."); 317 Svc.Obs.remove(firstNotification, onFirstSync); 318 Svc.Obs.add(secondNotification, onSecondSync); 319 320 Assert.equal(Service.clusterURL, ""); 321 322 // We got a 401 mid-sync, and set the pref accordingly. 323 Assert.ok(Services.prefs.getBoolPref("services.sync.lastSyncReassigned")); 324 325 // Update the timestamp. 326 now = Date.now(); 327 } 328 329 function onSecondSync() { 330 _("Second sync completed."); 331 Svc.Obs.remove(secondNotification, onSecondSync); 332 Svc.Obs.add(thirdNotification, onThirdSync); 333 334 // This sync occurred within the backoff interval. 335 let elapsedTime = Date.now() - now; 336 Assert.less(elapsedTime, MINIMUM_BACKOFF_INTERVAL); 337 338 // This pref will be true until a sync completes successfully. 339 Assert.ok(getReassigned()); 340 341 // The timer will be set for some distant time. 342 // We store nextSync in prefs, which offers us only limited resolution. 343 // Include that logic here. 344 let expectedNextSync = 345 1000 * Math.floor((now + MINIMUM_BACKOFF_INTERVAL) / 1000); 346 _("Next sync scheduled for " + Service.scheduler.nextSync); 347 _("Expected to be slightly greater than " + expectedNextSync); 348 349 Assert.greaterOrEqual(Service.scheduler.nextSync, expectedNextSync); 350 Assert.ok(!!Service.scheduler.syncTimer); 351 352 // Undo our evil scheme. 353 server.toplevelHandlers.storage = oldHandler; 354 355 // Bring the timer forward to kick off a successful sync, so we can watch 356 // the pref get cleared. 357 Service.scheduler.scheduleNextSync(0); 358 } 359 function onThirdSync() { 360 Svc.Obs.remove(thirdNotification, onThirdSync); 361 362 // That'll do for now; no more syncs. 363 Service.scheduler.clearSyncTriggers(); 364 365 // Make absolutely sure that any event listeners are done with their work 366 // before we proceed. 367 waitForZeroTimer(function () { 368 _("Third sync nextTick."); 369 Assert.ok(!getReassigned()); 370 Assert.equal(getTokenCount, 2); 371 Service.startOver().then(() => { 372 server.stop(deferred.resolve); 373 }); 374 }); 375 } 376 377 Svc.Obs.add(firstNotification, onFirstSync); 378 379 now = Date.now(); 380 await Service.sync(); 381 await deferred.promise; 382 }); 383 384 add_task(async function test_loop_avoidance_engine() { 385 enableValidationPrefs(); 386 387 _( 388 "Test that a repeated 401 in an engine doesn't result in a sync loop " + 389 "if node reassignment cannot resolve the failure." 390 ); 391 let server = await prepareServer(); 392 let john = server.user("johndoe"); 393 394 _("Enabling the Rotary engine."); 395 let { engine, syncID, tracker } = await registerRotaryEngine(); 396 let deferred = Promise.withResolvers(); 397 398 let getTokenCount = 0; 399 let mockTSC = { 400 // TokenServerClient 401 async getTokenUsingOAuth() { 402 getTokenCount++; 403 return { endpoint: server.baseURI + "1.1/johndoe/" }; 404 }, 405 }; 406 Service.identity._tokenServerClient = mockTSC; 407 408 // We need the server to be correctly set up prior to experimenting. Do this 409 // through a sync. 410 let global = { 411 syncID: Service.syncID, 412 storageVersion: STORAGE_VERSION, 413 rotary: { version: engine.version, syncID }, 414 }; 415 john.createCollection("meta").insert("global", global); 416 417 _("First sync to prepare server contents."); 418 await Service.sync(); 419 420 _("Setting up Rotary collection to 401."); 421 let rotary = john.createCollection("rotary"); 422 let oldHandler = rotary.collectionHandler; 423 rotary.collectionHandler = handleReassign.bind(this, undefined); 424 425 // Flag the Rotary engine to need syncing. 426 john.collection("rotary").timestamp += 1000; 427 428 function onLoginStart() { 429 // lastSyncReassigned shouldn't be cleared until a sync has succeeded. 430 _("Ensuring that lastSyncReassigned is still set at next sync start."); 431 Assert.ok(getReassigned()); 432 } 433 434 function beforeSuccessfulSync() { 435 _("Undoing test changes."); 436 rotary.collectionHandler = oldHandler; 437 } 438 439 let firstNotification = "weave:service:sync:finish"; 440 let secondNotification = "weave:service:sync:finish"; 441 let thirdNotification = "weave:service:sync:finish"; 442 443 // Track the time. We want to make sure the duration between the first and 444 // second sync is small, and then that the duration between second and third 445 // is set to be large. 446 let now; 447 448 function onFirstSync() { 449 _("First sync completed."); 450 Svc.Obs.remove(firstNotification, onFirstSync); 451 Svc.Obs.add(secondNotification, onSecondSync); 452 453 Assert.equal(Service.clusterURL, ""); 454 455 _("Adding observer that lastSyncReassigned is still set on login."); 456 Svc.Obs.add("weave:service:login:start", onLoginStart); 457 458 // We got a 401 mid-sync, and set the pref accordingly. 459 Assert.ok(Services.prefs.getBoolPref("services.sync.lastSyncReassigned")); 460 461 // Update the timestamp. 462 now = Date.now(); 463 } 464 465 function onSecondSync() { 466 _("Second sync completed."); 467 Svc.Obs.remove(secondNotification, onSecondSync); 468 Svc.Obs.add(thirdNotification, onThirdSync); 469 470 // This sync occurred within the backoff interval. 471 let elapsedTime = Date.now() - now; 472 Assert.less(elapsedTime, MINIMUM_BACKOFF_INTERVAL); 473 474 // This pref will be true until a sync completes successfully. 475 Assert.ok(getReassigned()); 476 477 // The timer will be set for some distant time. 478 // We store nextSync in prefs, which offers us only limited resolution. 479 // Include that logic here. 480 let expectedNextSync = 481 1000 * Math.floor((now + MINIMUM_BACKOFF_INTERVAL) / 1000); 482 _("Next sync scheduled for " + Service.scheduler.nextSync); 483 _("Expected to be slightly greater than " + expectedNextSync); 484 485 Assert.greaterOrEqual(Service.scheduler.nextSync, expectedNextSync); 486 Assert.ok(!!Service.scheduler.syncTimer); 487 488 // Undo our evil scheme. 489 beforeSuccessfulSync(); 490 491 // Bring the timer forward to kick off a successful sync, so we can watch 492 // the pref get cleared. 493 Service.scheduler.scheduleNextSync(0); 494 } 495 496 function onThirdSync() { 497 Svc.Obs.remove(thirdNotification, onThirdSync); 498 499 // That'll do for now; no more syncs. 500 Service.scheduler.clearSyncTriggers(); 501 502 // Make absolutely sure that any event listeners are done with their work 503 // before we proceed. 504 waitForZeroTimer(function () { 505 _("Third sync nextTick."); 506 Assert.ok(!getReassigned()); 507 Assert.equal(getTokenCount, 2); 508 Svc.Obs.remove("weave:service:login:start", onLoginStart); 509 Service.startOver().then(() => { 510 server.stop(deferred.resolve); 511 }); 512 }); 513 } 514 515 Svc.Obs.add(firstNotification, onFirstSync); 516 517 now = Date.now(); 518 await Service.sync(); 519 await deferred.promise; 520 521 await tracker.clearChangedIDs(); 522 await Service.engineManager.unregister(engine); 523 });