History

* Switch trigger for deferred loads from timer to using leading entity completion * Bring back notion of tracking "bad scripts" to fail stampeding entities as a set * Update stampede JS tests to highlight/troubleshoot preexisting unload issue

humbletim committed 9 years ago

Commit 3ac11651 parent 02827555 files · +128 −96Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 5 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+93−91
@@ -1496,67 +1496,58 @@ const auto RETRY_ATTEMPT_BATCH = 100; //glm::clamp(qgetenv("RETRY_ATTEMPT_BATCH" · 1495 unmodified lines
1496const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000);1496const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000);
1497const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };1497const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
1498 1498
1499void ScriptEngine::deferLoadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {1499void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
1500 const auto needTimer = _pendingLoadEntities.isEmpty();1500 QList<DeferredLoadEntity> retryLoads;
1501 // NOTE: forceRedownload isn't forwarded here because deferLoadEntityScript() is only ever called when another entity has taken lead for the script1501 QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
1502 _pendingLoadEntities.push_back({ entityID, entityScript/*, forceRedownload*/ });1502 while (i.hasNext()) {
1503 1503 auto retry = i.next();
1504 auto processDeferredEntities = [this](QScriptContext* context, QScriptEngine*) -> QScriptValue {1504 if (retry.entityScript == entityScript) {
1505 QList<DeferredLoadEntity> stillPending;1505 retryLoads << retry;
1506 1506 i.remove();
1507 for (int i=0; i < RETRY_ATTEMPT_BATCH && !_pendingLoadEntities.isEmpty(); i++) {
1508 auto retry = _pendingLoadEntities.takeFirst();
1509 qCDebug(scriptengine) << "deferLoadEntityScript.retry[" << i << "]:" << retry.entityID << retry.entityScript;
1510 if (!_entityScripts.contains(retry.entityID)) {
1511 qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity details gone (entity deleted?)";
1512 } else {
1513 auto details = _entityScripts[retry.entityID];
1514 if (details.status != EntityScriptStatus::PENDING) {
1515 qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity status no longer PENDING; " << details.status;
1516 } else {
1517 if (!_occupiedScriptURLs.contains(retry.entityScript)) {
1518 // the URL is no longer busy with another Entity, so now it's our turn
1519 loadEntityScript(retry.entityID, retry.entityScript, false);
1520 } else {
1521 auto entityID = _occupiedScriptURLs[retry.entityScript];
1522 if (entityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
1523 // the URL was marked bad by an earlier load attempt, so abort the retry mission
1524 qCDebug(scriptengine) << "... pending load of " << retry.entityID << " cancelled";
1525 } else {
1526 // the URL is still occupied by another Entity, so add to next retry pass
1527 stillPending << retry;
1528 }
1529 }
1530 }
1531 }
1532 }
1533 foreach(const DeferredLoadEntity& retry, stillPending) {
1534 _pendingLoadEntities.push_back(retry);
1535 }1507 }
1536 if (_pendingLoadEntities.isEmpty()) {1508 }
1537 QObject *interval = context->callee().property("interval").toQObject();1509 foreach(auto &retry, retryLoads) {
1538 // we're done processing so can kill the timer1510 // check whether entity was since been deleted
1539 qCDebug(scriptengine) << "loadEntityScript.retry queue processing complete -- exiting" << interval;1511 if (!_entityScripts.contains(retry.entityID)) {
1540 if (interval) {1512 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)"
1541 clearInterval(interval);1513 << retry.entityID;
1542 }1514 continue;
1543 }1515 }
1544 return QScriptValue();
1545 };
1546 1516
1547 if (needTimer) {1517 // check whether entity has since been unloaded or otherwise errored-out
1548 // we were the first to get queued -- so set up an interval timer for deferred processing1518 auto details = _entityScripts[retry.entityID];
1549 qCDebug(scriptengine) << "deferLoadEntityScript -- scheduling timer " << RETRY_ATTEMPT_MSECS;1519 if (details.status != EntityScriptStatus::PENDING) {
1520 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; "
1521 << retry.entityID << details.status;
1522 continue;
1523 }
1550 1524
1551 // processDeferredEntities gets wrapped as a "regular" JavaScript callback so that it can then1525 // propagate leader's failure reasons to the pending entity
1552 // be scheduled using setInterval per usual -- allowing the script engine to own its lifecycle.1526 const auto leaderDetails = _entityScripts[leaderID];
1553 // we stil need to call clearInterval manually when done, so its passed to callee() above as a property.1527 if (leaderDetails.status != EntityScriptStatus::RUNNING) {
1528 qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")
1529 .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);
1530
1531 auto extraDetail = "\n(propagated from " + leaderID.toString() + ")";
1532 if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||
1533 leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {
1534 // propagate same error so User doesn't have to hunt down stampede's leader
1535 updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);
1536 } else {
1537 // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)
1538 updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1539 "A previous Entity failed to load using this script URL; reload to try again." + extraDetail);
1540 }
1541 continue;
1542 }
1554 1543
1555 QScriptValue function = newLambdaFunction(processDeferredEntities, "processDeferredEntities-loader");1544 if (_occupiedScriptURLs.contains(retry.entityScript)) {
1556 QObject *interval = setInterval( function, RETRY_ATTEMPT_MSECS );1545 qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;
1546 continue;
1547 }
1557 1548
1558 // make the interval reachable from the handler1549 // if we made it here then the leading entity was successful so proceed with normal load
1559 function.setProperty("interval", newQObject(interval));1550 loadEntityScript(retry.entityID, retry.entityScript, false);
1560 }1551 }
1561}1552}
1562 1553
@@ -1587,28 +1578,33 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 24 unmodified lines
1587 // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking1578 // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
1588 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back1579 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
1589 // before the first one has time to finish loading.1580 // before the first one has time to finish loading.
1590 if (!_occupiedScriptURLs.contains(entityScript)) {1581 if (_occupiedScriptURLs.contains(entityScript)) {
1591 // the scriptURL is available -- flag as in-use and proceed
1592 _occupiedScriptURLs[entityScript] = entityID;
1593 } else {
1594 // another Entity is either still booting from this script URL or has failed and marked it "out of order"
1595 auto currentEntityID = _occupiedScriptURLs[entityScript];1582 auto currentEntityID = _occupiedScriptURLs[entityScript];
1596 if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER && !forceRedownload) {1583 if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
1597 // since not force-reloading, assume that the exact same input would produce the exact same output again1584 if (forceRedownload) {
1598 // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.1585 // script was previously marked unusable, but we're reloading so reset it
1599 qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")1586 _occupiedScriptURLs.remove(entityScript);
1600 .arg(entityID.toString()).arg(entityScript);1587 } else {
1601 updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,1588 // since not reloading, assume that the exact same input would produce the exact same output again
1602 "Cached Script was marked invalid by previous Entity load failure.");1589 // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.
1590 qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")
1591 .arg(entityID.toString()).arg(entityScript);
1592 updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1593 "A previous Entity failed to load using this script URL; reload to try again.");
1594 return;
1595 }
1603 } else {1596 } else {
1604 // another entity is busy loading from this script URL so wait for them to finish1597 // another entity is busy loading from this script URL so wait for them to finish
1605 qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2")1598 qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")
1606 .arg( _pendingLoadEntities.size()).arg(entityID.toString()).arg(entityScript);1599 .arg( _deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());
1607 deferLoadEntityScript(entityID, entityScript, forceRedownload);1600 _deferredEntityLoads.push_back({ entityID, entityScript });
1601 return;
1608 }1602 }
1609 return;
1610 }1603 }
1611 1604
1605 // the scriptURL slot is available; flag as in-use
1606 _occupiedScriptURLs[entityScript] = entityID;
1607
1612#ifdef DEBUG_ENTITY_STATES1608#ifdef DEBUG_ENTITY_STATES
1613 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;1609 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
1614 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()1610 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
@@ -1642,7 +1638,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& · 27 unmodified lines
1642#endif1638#endif
1643 }1639 }
1644 // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable1640 // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable
1645 if (_occupiedScriptURLs[entityScript] == entityID) {1641 if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {
1646 _occupiedScriptURLs.remove(entityScript);1642 _occupiedScriptURLs.remove(entityScript);
1647 }1643 }
1648 });1644 });
@@ -1679,6 +1675,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 30 unmodified lines
1679 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";1675 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
1680 1676
1681 const EntityScriptDetails &oldDetails = _entityScripts[entityID];1677 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1678 const auto entityScript = oldDetails.scriptText;
1679
1682 EntityScriptDetails newDetails;1680 EntityScriptDetails newDetails;
1683 newDetails.scriptText = scriptOrURL;1681 newDetails.scriptText = scriptOrURL;
1684 1682
@@ -1689,10 +1687,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
1689 newDetails.status = status;1687 newDetails.status = status;
1690 setEntityScriptDetails(entityID, newDetails);1688 setEntityScriptDetails(entityID, newDetails);
1691 1689
1692 qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << oldDetails.scriptText << " as BAD_SCRIPT_UUID_PLACEHOLDER";1690 qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";
1693 // flag this scriptURL as unusuable1691 // flag the original entityScript as unusuable
1694 // note: we want the *original* entityScript string (not the potentially normalized one from ScriptCache results)1692 _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;
1695 _occupiedScriptURLs[oldDetails.scriptText] = BAD_SCRIPT_UUID_PLACEHOLDER;1693 processDeferredEntityLoads(entityScript, entityID);
1696 };1694 };
1697 1695
1698 // NETWORK / FILESYSTEM ERRORS1696 // NETWORK / FILESYSTEM ERRORS
@@ -1826,6 +1824,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 127 unmodified lines
1826 1824
1827 // if we got this far, then call the preload method1825 // if we got this far, then call the preload method
1828 callEntityScriptMethod(entityID, "preload");1826 callEntityScriptMethod(entityID, "preload");
1827
1828 _occupiedScriptURLs.remove(entityScript);
1829 processDeferredEntityLoads(entityScript, entityID);
1829}1830}
1830 1831
1831void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {1832void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@@ -1845,14 +1846,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines
1845#endif1846#endif
1846 1847
1847 if (_entityScripts.contains(entityID)) {1848 if (_entityScripts.contains(entityID)) {
1849 auto oldDetails = _entityScripts[entityID];
1848 if (isEntityScriptRunning(entityID)) {1850 if (isEntityScriptRunning(entityID)) {
1849 callEntityScriptMethod(entityID, "unload");1851 callEntityScriptMethod(entityID, "unload");
1852 } else {
1853 qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
1854 }
1855 if (oldDetails.status != EntityScriptStatus::UNLOADED) {
1856 EntityScriptDetails newDetails;
1857 newDetails.status = EntityScriptStatus::UNLOADED;
1858 newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1859 // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
1860 newDetails.scriptText = oldDetails.scriptText;
1861 setEntityScriptDetails(entityID, newDetails);
1850 }1862 }
1851 EntityScriptDetails newDetails;
1852 newDetails.status = EntityScriptStatus::UNLOADED;
1853 newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1854 setEntityScriptDetails(entityID, newDetails);
1855 stopAllTimersForEntityScript(entityID);1863 stopAllTimersForEntityScript(entityID);
1864 {
1865 // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
1866 processDeferredEntityLoads(oldDetails.scriptText, entityID);
1867 }
1856 }1868 }
1857}1869}
1858 1870
@@ -1902,17 +1914,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 43 unmodified lines
1902 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();1914 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
1903 if (lastModified > details.lastModified) {1915 if (lastModified > details.lastModified) {
1904 scriptInfoMessage("Reloading modified script " + details.scriptText);1916 scriptInfoMessage("Reloading modified script " + details.scriptText);
1905 1917 loadEntityScript(entityID, details.scriptText, true);
1906 QFile file(filePath);
1907 file.open(QIODevice::ReadOnly);
1908 QString scriptContents = QTextStream(&file).readAll();
1909 this->unloadEntityScript(entityID);
1910 this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success");
1911 if (!isEntityScriptRunning(entityID)) {
1912 scriptWarningMessage("Reload script " + details.scriptText + " failed");
1913 } else {
1914 details = _entityScripts[entityID];
1915 }
1916 }1918 }
1917 }1919 }
1918 recurseGuard = false;1920 recurseGuard = false;