|
59 | 59 | $(function() { |
60 | 60 | var ROOT_URL = "@Url.Content("~/")"; |
61 | 61 |
|
| 62 | + // Set global AJAX timeout to prevent hanging requests |
| 63 | + $.ajaxSetup({ |
| 64 | + timeout: 30000 // 30 seconds timeout for all AJAX requests |
| 65 | + }); |
| 66 | +
|
| 67 | + // Send important logs to server for CI debugging |
| 68 | + var serverLog = function(message) { |
| 69 | + console.log(message); // Also log to browser console |
| 70 | + |
| 71 | + // Send to server if in CI mode |
| 72 | + if(@Html.Raw(Json.Serialize(Model.IsContinuousIntegration))) { |
| 73 | + $.post(@Html.Raw(Json.Serialize(Url.Action("LogMiscError"))), { |
| 74 | + msg: "[JS] " + message, |
| 75 | + timeout: 5000 // Short timeout for logging |
| 76 | + }).fail(function() { |
| 77 | + // Silently ignore failures to avoid recursion |
| 78 | + }); |
| 79 | + } |
| 80 | + }; |
| 81 | +
|
62 | 82 | var TEST_TIMEOUT_SECONDS = 45, |
63 | 83 | TEST_TIMEOUT = TEST_TIMEOUT_SECONDS * 1000, |
64 | | - WORKER_STUCK_TIMEOUT = 120 * 1000, // 2 minutes without any activity |
| 84 | + WORKER_STUCK_TIMEOUT = 60 * 1000, // 60 seconds without any activity |
65 | 85 | WORKER_NAME_PREFIX = "workerFrame", |
66 | 86 | busyCount = 0, |
67 | 87 | suitesDescription = { |
|
129 | 149 | url: @Html.Raw(Json.Serialize(Url.Action("SaveResults"))), |
130 | 150 | type: "post", |
131 | 151 | contentType: "application/json", |
132 | | - data: JSON.stringify(testResults) |
| 152 | + data: JSON.stringify(testResults), |
| 153 | + timeout: 60000 // 60 second timeout for saving results |
133 | 154 | }).done(function() { |
134 | 155 | notifyDeviceTestManager("QUnit.saveResults.done"); |
135 | 156 | removeWorkers(); |
136 | 157 |
|
137 | 158 | var frame = document.getElementById("reportFrame"); |
138 | 159 | frame.style.display = "block"; |
139 | 160 | frame.setAttribute("src", @Html.Raw(Json.Serialize(Url.Action("DisplayResults")))); |
| 161 | + }).fail(function(jqXHR, textStatus, errorThrown) { |
| 162 | + serverLog('SaveResults FAILED: ' + textStatus + ' ' + errorThrown); |
| 163 | + // Try to continue anyway |
| 164 | + notifyDeviceTestManager("QUnit.saveResults.failed"); |
| 165 | + removeWorkers(); |
140 | 166 | }); |
141 | 167 | }; |
142 | 168 |
|
|
148 | 174 | |
149 | 175 | // Safety check: if worker is still busy, do not load next test |
150 | 176 | if(suitesInProgress[i] !== null && suitesInProgress[i] !== undefined) { |
151 | | - console.error('nextUrl: worker ' + i + ' is still busy with ' + suitesInProgress[i].name + '! Skipping.'); |
| 177 | + serverLog('ERROR: nextUrl called but worker ' + i + ' is still busy with ' + suitesInProgress[i].name + '! Skipping.'); |
152 | 178 | return; |
153 | 179 | } |
154 | 180 | |
|
220 | 246 |
|
221 | 247 | finalize: function(success) { |
222 | 248 | if(this.finalized) { |
223 | | - console.warn('Suite already finalized: ' + this.name + ', skipping duplicate finalize'); |
| 249 | + serverLog('Suite already finalized: ' + this.name + ', skipping duplicate finalize'); |
224 | 250 | return; |
225 | 251 | } |
226 | 252 | |
227 | 253 | var suiteName = this.name; // Save name before cleanup |
| 254 | + serverLog('===== FINALIZE CALLED for ' + suiteName + ' ====='); |
228 | 255 | |
229 | 256 | this.finalized = true; |
230 | 257 | this.time = roundTime((new Date() - this.startTime) / 1000); |
|
238 | 265 | suitesInProgress[i] = null; |
239 | 266 | busyCount--; |
240 | 267 | |
241 | | - console.log('Suite finalized: ' + suiteName + ', worker=' + _i + ', busyCount=' + busyCount + |
| 268 | + serverLog('Suite finalized: ' + suiteName + ', worker=' + _i + ', busyCount=' + busyCount + |
242 | 269 | ', scheduling nextUrl in worker ' + _i); |
243 | 270 |
|
244 | 271 | setTimeout(function() { |
245 | | - console.log('Calling nextUrl for worker ' + _i + ' after finalizing ' + suiteName); |
| 272 | + serverLog('Calling nextUrl for worker ' + _i + ' after finalizing ' + suiteName); |
246 | 273 | nextUrl.call(that, _i); |
247 | 274 | }, 0); |
248 | 275 | } |
|
306 | 333 | activeWorkers.push('Worker' + i + ':' + suite.name + '(' + inactiveSeconds + 's)'); |
307 | 334 | |
308 | 335 | if(lastActivity && (now - lastActivity) > WORKER_STUCK_TIMEOUT) { |
309 | | - console.error('Worker ' + i + ' is STUCK on test: ' + suite.name + ' (no activity for ' + |
| 336 | + serverLog('ERROR: Worker ' + i + ' is STUCK on test: ' + suite.name + ' (no activity for ' + |
310 | 337 | inactiveSeconds + ' seconds), busyCount=' + busyCount); |
311 | | - console.log('Force finalizing stuck worker ' + i); |
| 338 | + serverLog('Force finalizing stuck worker ' + i + ' and loading next test'); |
312 | 339 | |
313 | 340 | // Force finalize the stuck suite (finalize checks for double-finalization) |
314 | 341 | suite.finalize(false); // Mark as failed |
315 | 342 | |
316 | | - // Reset worker state |
| 343 | + // Clear worker state |
| 344 | + suitesInProgress[i] = null; |
317 | 345 | workerLastActivity[i] = now; |
| 346 | + |
| 347 | + // Try to reload the worker by loading next test |
| 348 | + var worker = workerByIndex(i); |
| 349 | + if(worker) { |
| 350 | + try { |
| 351 | + // Force stop any running tests in the iframe |
| 352 | + if(worker.contentWindow && worker.contentWindow.stop) { |
| 353 | + worker.contentWindow.stop(); |
| 354 | + } |
| 355 | + } catch(e) { |
| 356 | + serverLog('Could not stop worker iframe: ' + e.message); |
| 357 | + } |
| 358 | + |
| 359 | + // Load next test |
| 360 | + setTimeout(function() { |
| 361 | + serverLog('Loading next test for recovered worker ' + i); |
| 362 | + nextUrl(i); |
| 363 | + }, 100); |
| 364 | + } |
318 | 365 | } |
319 | 366 | } |
320 | 367 | } |
|
503 | 550 | }; |
504 | 551 |
|
505 | 552 | window.RUNNER_ON_DONE = function(worker, qunitData) { |
| 553 | + serverLog('===== RUNNER_ON_DONE CALLED ====='); |
| 554 | + |
506 | 555 | var i = indexFromWorkerName(worker), |
507 | 556 | suite = suitesInProgress[i], |
508 | 557 | passed = !qunitData.failed; |
509 | 558 |
|
510 | | - console.log('RUNNER_ON_DONE: worker=' + i + ', suite=' + (suite ? suite.name : 'null') + |
| 559 | + serverLog('RUNNER_ON_DONE: worker=' + i + ', suite=' + (suite ? suite.name : 'null') + |
511 | 560 | ', busyCount=' + busyCount + ', failed=' + qunitData.failed + ', total=' + qunitData.total); |
512 | 561 |
|
513 | 562 | if(suite) { |
|
519 | 568 | } |
520 | 569 | |
521 | 570 | var suiteName = suite.name; // Save before finalize clears it |
| 571 | + serverLog('RUNNER_ON_DONE: About to finalize suite ' + suiteName + ' (worker=' + i + ')'); |
522 | 572 | |
523 | 573 | // finalize() handles busyCount-- internally and prevents double-finalization |
524 | 574 | suite.finalize(passed, qunitData.total); |
| 575 | + |
| 576 | + serverLog('RUNNER_ON_DONE: About to notify server about ' + suiteName + ' completion'); |
525 | 577 | notifySuiteFinalized(suiteName, passed, qunitData.runtime); |
526 | 578 | } else { |
527 | 579 | console.warn('RUNNER_ON_DONE: suite is null for worker ' + i + ' - already finalized, skipping nextUrl call'); |
|
560 | 612 | } |
561 | 613 |
|
562 | 614 | function notifySuiteFinalized(name, passed, runtime) { |
563 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), { name: name, passed: passed, runtime: runtime }) |
564 | | - .fail(function(jqXHR, textStatus, errorThrown) { |
565 | | - console.warn('NotifySuiteFinalized failed:', textStatus, errorThrown); |
566 | | - }); |
| 615 | + serverLog('notifySuiteFinalized: Sending POST for ' + name); |
| 616 | + $.ajax({ |
| 617 | + url: @Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), |
| 618 | + type: 'POST', |
| 619 | + data: { name: name, passed: passed, runtime: runtime }, |
| 620 | + timeout: 15000, // 15 second timeout for suite finalization |
| 621 | + success: function() { |
| 622 | + serverLog('notifySuiteFinalized: Server responded OK for ' + name); |
| 623 | + }, |
| 624 | + error: function(jqXHR, textStatus, errorThrown) { |
| 625 | + serverLog('NotifySuiteFinalized FAILED for ' + name + ': ' + textStatus + ' ' + errorThrown); |
| 626 | + // Continue despite failure - don't block the worker |
| 627 | + } |
| 628 | + }); |
567 | 629 | } |
568 | 630 | function notifyIsAlive(){ |
569 | 631 | $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyIsAlive")))) |
|
572 | 634 | }); |
573 | 635 | } |
574 | 636 |
|
575 | | - // Check for stuck workers every 30 seconds |
576 | | - setInterval(checkStuckWorkers, 30000); |
| 637 | + // Check for stuck workers every 15 seconds |
| 638 | + setInterval(checkStuckWorkers, 15000); |
| 639 | + |
| 640 | + // Log overall state every 10 seconds for debugging |
| 641 | + setInterval(function() { |
| 642 | + var activeSuites = []; |
| 643 | + for(var i = 0; i < WORKER_COUNT; i++) { |
| 644 | + if(suitesInProgress[i]) { |
| 645 | + activeSuites.push('Worker' + i + ':' + suitesInProgress[i].name); |
| 646 | + } |
| 647 | + } |
| 648 | + |
| 649 | + // Only log to server if there are active suites or issues |
| 650 | + if(activeSuites.length > 0 || busyCount > 0 || (urls.length === 0 && !resultSaving)) { |
| 651 | + var stateMsg = '===== STATE: busyCount=' + busyCount + ', urls=' + urls.length + |
| 652 | + ', resultSaving=' + resultSaving + ', active=' + (activeSuites.length > 0 ? activeSuites.join('; ') : 'none'); |
| 653 | + serverLog(stateMsg); |
| 654 | + } |
| 655 | + }, 10000); |
577 | 656 |
|
578 | 657 | function roundTime(time) { |
579 | 658 | return +(time.toFixed(3)); |
|
585 | 664 | notifyIsAlive(); |
586 | 665 | }, 30000); // Reduced from 60000ms to 30000ms (30 seconds) to prevent watchdog timeout |
587 | 666 | |
| 667 | + serverLog('Starting test run with ' + WORKER_COUNT + ' workers, ' + urls.length + ' tests'); |
588 | 668 | runFirstBatch(); |
589 | 669 | }); |
590 | 670 | </script> |
|
0 commit comments