//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2016 ArangoDB GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Dr. Frank Celler //////////////////////////////////////////////////////////////////////////////// #include "V8DealerFeature.h" #include #include #include "Actions/ActionFeature.h" #include "Actions/actions.h" #include "ApplicationFeatures/V8PlatformFeature.h" #include "ApplicationFeatures/V8SecurityFeature.h" #include "Basics/ArangoGlobalContext.h" #include "Basics/ConditionLocker.h" #include "Basics/FileUtils.h" #include "Basics/ScopeGuard.h" #include "Basics/StringUtils.h" #include "Basics/Thread.h" #include "Basics/TimedAction.h" #include "Basics/application-exit.h" #include "Basics/files.h" #include "Cluster/ClusterFeature.h" #include "Cluster/ServerState.h" #include "FeaturePhases/ClusterFeaturePhase.h" #include "Logger/LogMacros.h" #include "Logger/Logger.h" #include "Logger/LoggerStream.h" #include "ProgramOptions/ProgramOptions.h" #include "ProgramOptions/Section.h" #include "Random/RandomGenerator.h" #include "Rest/Version.h" #include "RestServer/DatabasePathFeature.h" #include "RestServer/FrontendFeature.h" #include "RestServer/ScriptFeature.h" #include "RestServer/SystemDatabaseFeature.h" #include "Scheduler/SchedulerFeature.h" #include "Transaction/V8Context.h" #include "V8/JavaScriptSecurityContext.h" #include "V8/v8-buffer.h" #include "V8/v8-conv.h" #include "V8/v8-globals.h" #include "V8/v8-shell.h" #include "V8/v8-utils.h" #include "V8Server/FoxxQueuesFeature.h" #include "V8Server/V8Context.h" #include "V8Server/v8-actions.h" #include "V8Server/v8-ttl.h" #include "V8Server/v8-user-functions.h" #include "V8Server/v8-user-structures.h" #include "VocBase/vocbase.h" using namespace arangodb; using namespace arangodb::application_features; using namespace arangodb::basics; using namespace arangodb::options; V8DealerFeature* V8DealerFeature::DEALER = nullptr; namespace { class V8GcThread : public Thread { public: explicit V8GcThread(V8DealerFeature& dealer) : Thread(dealer.server(), "V8GarbageCollector"), _dealer(dealer), _lastGcStamp(static_cast(TRI_microtime())) {} ~V8GcThread() { shutdown(); } public: void run() override { _dealer.collectGarbage(); } double getLastGcStamp() { return static_cast(_lastGcStamp.load(std::memory_order_acquire)); } void updateGcStamp(double value) { _lastGcStamp.store(static_cast(value), std::memory_order_release); } private: V8DealerFeature& _dealer; std::atomic _lastGcStamp; }; } // namespace V8DealerFeature::V8DealerFeature(application_features::ApplicationServer& server) : application_features::ApplicationFeature(server, "V8Dealer"), _gcFrequency(60.0), _gcInterval(2000), _maxContextAge(60.0), _copyInstallation(false), _nrMaxContexts(0), _nrMinContexts(0), _nrInflightContexts(0), _maxContextInvocations(0), _allowAdminExecute(false), _enableJS(true), _nextId(0), _stopping(false), _gcFinished(false), _dynamicContextCreationBlockers(0) { setOptional(true); startsAfter(); startsAfter(); startsAfter(); startsAfter(); } void V8DealerFeature::collectOptions(std::shared_ptr options) { options->addSection("javascript", "Configure the Javascript engine"); options->addOption( "--javascript.gc-frequency", "JavaScript time-based garbage collection frequency (each x seconds)", new DoubleParameter(&_gcFrequency), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption( "--javascript.gc-interval", "JavaScript request-based garbage collection interval (each x requests)", new UInt64Parameter(&_gcInterval), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption("--javascript.app-path", "directory for Foxx applications", new StringParameter(&_appPath)); options->addOption( "--javascript.startup-directory", "path to the directory containing JavaScript startup scripts", new StringParameter(&_startupDirectory)); options->addOption("--javascript.module-directory", "additional paths containing JavaScript modules", new VectorParameter(&_moduleDirectories), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption( "--javascript.copy-installation", "copy contents of 'javascript.startup-directory' on first start", new BooleanParameter(&_copyInstallation)); options->addOption("--javascript.v8-contexts", "maximum number of V8 contexts that are created for " "executing JavaScript actions", new UInt64Parameter(&_nrMaxContexts)); options->addOption("--javascript.v8-contexts-minimum", "minimum number of V8 contexts that keep available for " "executing JavaScript actions", new UInt64Parameter(&_nrMinContexts)); options->addOption( "--javascript.v8-contexts-max-invocations", "maximum number of invocations for each V8 context before it is disposed", new UInt64Parameter(&_maxContextInvocations), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption( "--javascript.v8-contexts-max-age", "maximum age for each V8 context (in seconds) before it is disposed", new DoubleParameter(&_maxContextAge), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption( "--javascript.allow-admin-execute", "for testing purposes allow '_admin/execute', NEVER enable on production", new BooleanParameter(&_allowAdminExecute), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); options->addOption("--javascript.enabled", "enable the V8 JavaScript engine", new BooleanParameter(&_enableJS), arangodb::options::makeFlags(arangodb::options::Flags::Hidden)); } void V8DealerFeature::validateOptions(std::shared_ptr options) { ProgramOptions::ProcessingResult const& result = options->processingResult(); V8SecurityFeature& v8security = server().getFeature(); // DBServer and Agent don't need JS. Agent role handled in AgencyFeature if (ServerState::instance()->getRole() == ServerState::RoleEnum::ROLE_DBSERVER && (!result.touched("console") || !*(options->get("console")->ptr))) { // specifiying --console requires JavaScript, so we can only turn it off // if not specified _enableJS = false; } if (!_enableJS) { disable(); server().disableFeatures( std::vector{std::type_index(typeid(V8PlatformFeature)), std::type_index(typeid(ActionFeature)), std::type_index(typeid(ScriptFeature)), std::type_index(typeid(FoxxQueuesFeature)), std::type_index(typeid(FrontendFeature))}); return; } // check the startup path if (_startupDirectory.empty()) { LOG_TOPIC("6330a", FATAL, arangodb::Logger::V8) << "no 'javascript.startup-directory' has been supplied, giving up"; FATAL_ERROR_EXIT(); } // remove trailing / from path and set path auto ctx = ArangoGlobalContext::CONTEXT; if (ctx == nullptr) { LOG_TOPIC("ae845", FATAL, arangodb::Logger::V8) << "failed to get global context"; FATAL_ERROR_EXIT(); } ctx->normalizePath(_startupDirectory, "javascript.startup-directory", true); v8security.addToInternalWhitelist(_startupDirectory, FSAccessType::READ); ctx->normalizePath(_moduleDirectories, "javascript.module-directory", false); // try to append the current version name to the startup directory, // so instead of "/path/to/js" we will get "/path/to/js/3.4.0" std::string const versionAppendix = std::regex_replace(rest::Version::getServerVersion(), std::regex("-.*$"), ""); std::string versionedPath = basics::FileUtils::buildFilename(_startupDirectory, versionAppendix); LOG_TOPIC("604da", DEBUG, Logger::V8) << "checking for existence of version-specific startup-directory '" << versionedPath << "'"; if (basics::FileUtils::isDirectory(versionedPath)) { // version-specific js path exists! _startupDirectory = versionedPath; } for (auto& it : _moduleDirectories) { versionedPath = basics::FileUtils::buildFilename(it, versionAppendix); LOG_TOPIC("8e21a", DEBUG, Logger::V8) << "checking for existence of version-specific module-directory '" << versionedPath << "'"; if (basics::FileUtils::isDirectory(versionedPath)) { // version-specific js path exists! it = versionedPath; } v8security.addToInternalWhitelist(it, FSAccessType::READ); } // check whether app-path was specified if (_appPath.empty()) { LOG_TOPIC("a161b", FATAL, arangodb::Logger::V8) << "no value has been specified for --javascript.app-path"; FATAL_ERROR_EXIT(); } // Tests if this path is either a directory (ok) or does not exist (we create // it in ::start) If it is something else this will throw an error. ctx->normalizePath(_appPath, "javascript.app-path", false); v8security.addToInternalWhitelist(_appPath, FSAccessType::READ); v8security.addToInternalWhitelist(_appPath, FSAccessType::WRITE); v8security.dumpAccessLists(); // use a minimum of 1 second for GC if (_gcFrequency < 1) { _gcFrequency = 1; } } void V8DealerFeature::prepare() { auto& cluster = server().getFeature(); defineDouble("SYS_DEFAULT_REPLICATION_FACTOR_SYSTEM", cluster.systemReplicationFactor()); } void V8DealerFeature::start() { if (_copyInstallation) { copyInstallationFiles(); // will exit process if it fails } else { // don't copy JS files on startup // now check if we have a js directory inside the database directory, and if // it looks good auto& dbPathFeature = server().getFeature(); const std::string dbJSPath = FileUtils::buildFilename(dbPathFeature.directory(), "js"); const std::string checksumFile = FileUtils::buildFilename(dbJSPath, StaticStrings::checksumFileJs); const std::string serverPath = FileUtils::buildFilename(dbJSPath, "server"); const std::string commonPath = FileUtils::buildFilename(dbJSPath, "common"); if (FileUtils::isDirectory(dbJSPath) && FileUtils::exists(checksumFile) && FileUtils::isDirectory(serverPath) && FileUtils::isDirectory(commonPath)) { // only load node modules from original startup path _nodeModulesDirectory = _startupDirectory; // js directory inside database directory looks good. now use it! _startupDirectory = dbJSPath; } } LOG_TOPIC("77c97", DEBUG, Logger::V8) << "effective startup-directory: " << _startupDirectory << ", effective module-directories: " << _moduleDirectories << ", node-modules-directory: " << _nodeModulesDirectory; _startupLoader.setDirectory(_startupDirectory); ServerState::instance()->setJavaScriptPath(_startupDirectory); // dump paths { std::vector paths; paths.push_back(std::string("startup '" + _startupDirectory + "'")); if (!_moduleDirectories.empty()) { paths.push_back( std::string("module '" + StringUtils::join(_moduleDirectories, ";") + "'")); } if (!_appPath.empty()) { paths.push_back(std::string("application '" + _appPath + "'")); // create app directory if it does not exist if (!basics::FileUtils::isDirectory(_appPath)) { std::string systemErrorStr; long errorNo; int res = TRI_CreateRecursiveDirectory(_appPath.c_str(), errorNo, systemErrorStr); if (res == TRI_ERROR_NO_ERROR) { LOG_TOPIC("86aa0", INFO, arangodb::Logger::FIXME) << "created javascript.app-path directory '" << _appPath << "'"; } else { LOG_TOPIC("2d23f", FATAL, arangodb::Logger::FIXME) << "unable to create javascript.app-path directory '" << _appPath << "': " << systemErrorStr; FATAL_ERROR_EXIT(); } } } LOG_TOPIC("86632", INFO, arangodb::Logger::V8) << "JavaScript using " << StringUtils::join(paths, ", "); } // set singleton DEALER = this; if (_nrMinContexts < 1) { _nrMinContexts = 1; } // try to guess a suitable number of contexts if (0 == _nrMaxContexts) { // automatic maximum number of contexts should not be below 16 // this is because the number of cores may be too few for the cluster // startup to properly run through with all its parallel requests // and the potential need for multiple V8 contexts _nrMaxContexts = (std::max)(uint64_t(0 /*scheduler->concurrency()*/), uint64_t(16)); } if (_nrMinContexts > _nrMaxContexts) { // max contexts must not be lower than min contexts _nrMaxContexts = _nrMinContexts; } LOG_TOPIC("09e14", DEBUG, Logger::V8) << "number of V8 contexts: min: " << _nrMinContexts << ", max: " << _nrMaxContexts; defineDouble("V8_CONTEXTS", static_cast(_nrMaxContexts)); // setup instances { CONDITION_LOCKER(guard, _contextCondition); _contexts.reserve(static_cast(_nrMaxContexts)); _busyContexts.reserve(static_cast(_nrMaxContexts)); _idleContexts.reserve(static_cast(_nrMaxContexts)); _dirtyContexts.reserve(static_cast(_nrMaxContexts)); for (size_t i = 0; i < _nrMinContexts; ++i) { guard.unlock(); // avoid lock order inversion in buildContext V8Context* context = buildContext(nextId()); guard.lock(); TRI_ASSERT(context != nullptr); try { _contexts.push_back(context); } catch (...) { delete context; throw; } } TRI_ASSERT(_contexts.size() > 0); TRI_ASSERT(_contexts.size() <= _nrMaxContexts); for (auto& context : _contexts) { // apply context update is only run on contexts that no other // threads can see (yet) guard.unlock(); applyContextUpdate(context); guard.lock(); _idleContexts.push_back(context); } } auto& sysDbFeature = server().getFeature(); auto database = sysDbFeature.use(); loadJavaScriptFileInAllContexts(database.get(), "server/initialize.js", nullptr); startGarbageCollection(); } void V8DealerFeature::copyInstallationFiles() { if (!_enableJS && (ServerState::instance()->isAgent() || ServerState::instance()->isDBServer())) { // skip expensive file-copying in case we are an agency or db server // these do not need JavaScript support return; } // get base path from DatabasePathFeature auto& dbPathFeature = server().getFeature(); std::string const copyJSPath = FileUtils::buildFilename(dbPathFeature.directory(), "js"); if (copyJSPath == _startupDirectory) { LOG_TOPIC("89fe2", FATAL, arangodb::Logger::V8) << "'javascript.startup-directory' cannot be inside " "'database.directory'"; FATAL_ERROR_EXIT(); } TRI_ASSERT(!copyJSPath.empty()); _nodeModulesDirectory = _startupDirectory; const std::string checksumFile = FileUtils::buildFilename(_startupDirectory, StaticStrings::checksumFileJs); const std::string copyChecksumFile = FileUtils::buildFilename(copyJSPath, StaticStrings::checksumFileJs); bool overwriteCopy = false; if (!FileUtils::exists(copyJSPath) || !FileUtils::exists(checksumFile) || !FileUtils::exists(copyChecksumFile)) { overwriteCopy = true; } else { try { overwriteCopy = (FileUtils::slurp(copyChecksumFile) != FileUtils::slurp(checksumFile)); } catch (basics::Exception const& e) { LOG_TOPIC("efa47", ERR, Logger::V8) << "Error reading '" << StaticStrings::checksumFileJs << "' from disk: " << e.what(); overwriteCopy = true; } } if (overwriteCopy) { // sanity check before removing an existing directory: // check if for some reason we will be trying to remove the entire database // directory... if (FileUtils::exists(FileUtils::buildFilename(copyJSPath, "ENGINE"))) { LOG_TOPIC("214d1", FATAL, Logger::V8) << "JS installation path '" << copyJSPath << "' seems to be invalid"; FATAL_ERROR_EXIT(); } LOG_TOPIC("dd1c0", DEBUG, Logger::V8) << "Copying JS installation files from '" << _startupDirectory << "' to '" << copyJSPath << "'"; int res = TRI_ERROR_NO_ERROR; if (FileUtils::exists(copyJSPath)) { res = TRI_RemoveDirectory(copyJSPath.c_str()); if (res != TRI_ERROR_NO_ERROR) { LOG_TOPIC("1a20d", FATAL, Logger::V8) << "Error cleaning JS installation path '" << copyJSPath << "': " << TRI_errno_string(res); FATAL_ERROR_EXIT(); } } if (!FileUtils::createDirectory(copyJSPath, &res)) { LOG_TOPIC("b8c79", FATAL, Logger::V8) << "Error creating JS installation path '" << copyJSPath << "': " << TRI_errno_string(res); FATAL_ERROR_EXIT(); } // intentionally do not copy js/node/node_modules... // we avoid copying this directory because it contains 5000+ files at the // moment, and copying them one by one is darn slow at least on Windows... std::string const versionAppendix = std::regex_replace(rest::Version::getServerVersion(), std::regex("-.*$"), ""); std::string const nodeModulesPath = FileUtils::buildFilename("js", "node", "node_modules"); std::string const nodeModulesPathVersioned = basics::FileUtils::buildFilename("js", versionAppendix, "node", "node_modules"); std::regex const binRegex("[/\\\\]\\.bin[/\\\\]", std::regex::ECMAScript); auto filter = [&nodeModulesPath, &nodeModulesPathVersioned, &binRegex](std::string const& filename) -> bool { if (std::regex_search(filename, binRegex)) { // don't copy files in .bin return true; } std::string normalized = filename; FileUtils::normalizePath(normalized); if ((!nodeModulesPath.empty() && normalized.size() >= nodeModulesPath.size() && normalized.substr(normalized.size() - nodeModulesPath.size(), nodeModulesPath.size()) == nodeModulesPath) || (!nodeModulesPathVersioned.empty() && normalized.size() >= nodeModulesPathVersioned.size() && normalized.substr(normalized.size() - nodeModulesPathVersioned.size(), nodeModulesPathVersioned.size()) == nodeModulesPathVersioned)) { // filter it out! return true; } // let the file/directory pass through return false; }; std::string error; if (!FileUtils::copyRecursive(_startupDirectory, copyJSPath, filter, error)) { LOG_TOPIC("45261", FATAL, Logger::V8) << "Error copying JS installation files to '" << copyJSPath << "': " << error; FATAL_ERROR_EXIT(); } // attempt to copy enterprise JS files too. // only required for developer installations, not packages std::string const enterpriseJs = basics::FileUtils::buildFilename(_startupDirectory, "..", "enterprise", "js"); if (FileUtils::isDirectory(enterpriseJs)) { std::function const passAllFilter = [](std::string const&) { return false; }; if (!FileUtils::copyRecursive(enterpriseJs, copyJSPath, passAllFilter, error)) { LOG_TOPIC("ae9d3", WARN, Logger::V8) << "Error copying enterprise JS installation files to '" << copyJSPath << "': " << error; } } } _startupDirectory = copyJSPath; } V8Context* V8DealerFeature::addContext() { if (server().isStopping()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_SHUTTING_DOWN); } V8Context* context = buildContext(nextId()); try { // apply context update is only run on contexts that no other // threads can see (yet) applyContextUpdate(context); auto& sysDbFeature = server().getFeature(); auto database = sysDbFeature.use(); TRI_ASSERT(database != nullptr); // no other thread can use the context when we are here, as the // context has not been added to the global list of contexts yet loadJavaScriptFileInContext(database.get(), "server/initialize.js", context, nullptr); return context; } catch (...) { delete context; throw; } } void V8DealerFeature::unprepare() { shutdownContexts(); // delete GC thread after all action threads have been stopped _gcThread.reset(); DEALER = nullptr; } bool V8DealerFeature::addGlobalContextMethod(std::string const& method) { bool result = true; CONDITION_LOCKER(guard, _contextCondition); for (auto& context : _contexts) { try { if (!context->addGlobalContextMethod(method)) { result = false; } } catch (...) { result = false; } } return result; } void V8DealerFeature::collectGarbage() { V8GcThread* gc = static_cast(_gcThread.get()); TRI_ASSERT(gc != nullptr); // this flag will be set to true if we timed out waiting for a GC signal // if set to true, the next cycle will use a reduced wait time so the GC // can be performed more early for all dirty contexts. The flag is set // to false again once all contexts have been cleaned up and there is nothing // more to do bool useReducedWait = false; bool preferFree = false; // the time we'll wait for a signal uint64_t const regularWaitTime = static_cast(_gcFrequency * 1000.0 * 1000.0); // the time we'll wait for a signal when the previous wait timed out uint64_t const reducedWaitTime = static_cast(_gcFrequency * 1000.0 * 200.0); while (!_stopping) { try { V8Context* context = nullptr; bool wasDirty = false; { bool gotSignal = false; preferFree = !preferFree; CONDITION_LOCKER(guard, _contextCondition); if (_dirtyContexts.empty()) { uint64_t waitTime = useReducedWait ? reducedWaitTime : regularWaitTime; // we'll wait for a signal or a timeout gotSignal = guard.wait(waitTime); } if (preferFree && !_idleContexts.empty()) { context = pickFreeContextForGc(); } if (context == nullptr && !_dirtyContexts.empty()) { context = _dirtyContexts.back(); _dirtyContexts.pop_back(); if (context->invocationsSinceLastGc() < 50 && !context->_hasActiveExternals) { // don't collect this one yet. it doesn't have externals, so there // is no urge for garbage collection _idleContexts.emplace_back(context); context = nullptr; } else { wasDirty = true; } } if (context == nullptr && !preferFree && !gotSignal && !_idleContexts.empty()) { // we timed out waiting for a signal, so we have idle time that we can // spend on running the GC pro-actively // We'll pick one of the free contexts and clean it up context = pickFreeContextForGc(); } // there is no context to clean up, probably they all have been cleaned // up already. increase the wait time so we don't cycle too much in the // GC loop and waste CPU unnecessary useReducedWait = (context != nullptr); } // update last gc time double lastGc = TRI_microtime(); gc->updateGcStamp(lastGc); if (context != nullptr) { LOG_TOPIC("6bb08", TRACE, arangodb::Logger::V8) << "collecting V8 garbage in context #" << context->id() << ", invocations total: " << context->invocations() << ", invocations since last gc: " << context->invocationsSinceLastGc() << ", hasActive: " << context->_hasActiveExternals << ", wasDirty: " << wasDirty; bool hasActiveExternals = false; auto isolate = context->_isolate; { // this guard will lock and enter the isolate // and automatically exit and unlock it when it runs out of scope V8ContextEntryGuard contextGuard(context); v8::HandleScope scope(isolate); auto localContext = v8::Local::New(isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); context->assertLocked(); TRI_GET_GLOBALS(); v8g->_inForcedCollect = true; TRI_RunGarbageCollectionV8(isolate, 1.0); v8g->_inForcedCollect = false; hasActiveExternals = v8g->hasActiveExternals(); } localContext->Exit(); } // update garbage collection statistics context->_hasActiveExternals = hasActiveExternals; context->setCleaned(lastGc); { CONDITION_LOCKER(guard, _contextCondition); if (_contexts.size() > _nrMinContexts && !context->isDefault() && context->shouldBeRemoved(_maxContextAge, _maxContextInvocations) && _dynamicContextCreationBlockers == 0) { // remove the extra context as it is not needed anymore _contexts.erase(std::remove_if(_contexts.begin(), _contexts.end(), [&context](V8Context* c) { return (c->id() == context->id()); })); LOG_TOPIC("0a995", DEBUG, Logger::V8) << "removed superfluous V8 context #" << context->id() << ", number of contexts is now: " << _contexts.size(); guard.unlock(); shutdownContext(context); } else { // put it back into the free list if (wasDirty) { _idleContexts.emplace_back(context); } else { _idleContexts.insert(_idleContexts.begin(), context); } guard.broadcast(); } } } else { useReducedWait = true; // sanity } } catch (...) { // simply ignore errors here useReducedWait = false; } } _gcFinished = true; } void V8DealerFeature::unblockDynamicContextCreation() { CONDITION_LOCKER(guard, _contextCondition); TRI_ASSERT(_dynamicContextCreationBlockers > 0); --_dynamicContextCreationBlockers; } void V8DealerFeature::loadJavaScriptFileInAllContexts(TRI_vocbase_t* vocbase, std::string const& file, VPackBuilder* builder) { if (builder != nullptr) { builder->openArray(); } std::vector contexts; { CONDITION_LOCKER(guard, _contextCondition); while (_nrInflightContexts > 0) { // wait until all pending context creation requests have been satisified guard.wait(10000); } // copy the list of contexts into a local variable contexts = _contexts; // block the addition or removal of contexts ++_dynamicContextCreationBlockers; } TRI_DEFER(unblockDynamicContextCreation()); LOG_TOPIC("1364d", TRACE, Logger::V8) << "loading JavaScript file '" << file << "' in all (" << contexts.size() << ") V8 contexts"; // now safely scan the local copy of the contexts for (auto& context : contexts) { CONDITION_LOCKER(guard, _contextCondition); while (_busyContexts.find(context) != _busyContexts.end()) { // we must not enter the context if another thread is also using it... guard.wait(10000); } auto it = std::find(_dirtyContexts.begin(), _dirtyContexts.end(), context); if (it != _dirtyContexts.end()) { // context is in _dirtyContexts // remove it from there _dirtyContexts.erase(it); guard.unlock(); try { loadJavaScriptFileInContext(vocbase, file, context, builder); } catch (...) { guard.lock(); _dirtyContexts.push_back(context); throw; } // and re-insert it after we are done guard.lock(); _dirtyContexts.push_back(context); } else { // if the context is neither busy nor dirty, it must be idle auto it = std::find(_idleContexts.begin(), _idleContexts.end(), context); if (it != _idleContexts.end()) { // remove it from there _idleContexts.erase(it); guard.unlock(); try { loadJavaScriptFileInContext(vocbase, file, context, builder); } catch (...) { guard.lock(); _idleContexts.push_back(context); throw; } // and re-insert it after we are done guard.lock(); _idleContexts.push_back(context); } else { LOG_TOPIC("d3a7f", WARN, Logger::V8) << "v8 context #" << context->id() << " has disappeared"; } } } if (builder != nullptr) { builder->close(); } } void V8DealerFeature::startGarbageCollection() { TRI_ASSERT(_gcThread == nullptr); _gcThread.reset(new V8GcThread(*this)); _gcThread->start(); _gcFinished = false; } void V8DealerFeature::prepareLockedContext(TRI_vocbase_t* vocbase, V8Context* context, JavaScriptSecurityContext const& securityContext) { TRI_ASSERT(vocbase != nullptr); // when we get here, we should have a context and an isolate context->assertLocked(); auto isolate = context->_isolate; { v8::HandleScope scope(isolate); auto localContext = v8::Local::New(isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); context->assertLocked(); TRI_GET_GLOBALS(); // initialize the context data v8g->_vocbase = vocbase; v8g->_securityContext = securityContext; try { LOG_TOPIC("94226", TRACE, arangodb::Logger::V8) << "entering V8 context #" << context->id(); context->handleGlobalContextMethods(); } catch (...) { // ignore errors here } } } } /// @brief enter a V8 context /// currently returns a nullptr if no context can be acquired in time V8Context* V8DealerFeature::enterContext(TRI_vocbase_t* vocbase, JavaScriptSecurityContext const& securityContext) { TRI_ASSERT(vocbase != nullptr); if (_stopping) { return nullptr; } if (!vocbase->use()) { return nullptr; } TimedAction exitWhenNoContext( [](double waitTime) { LOG_TOPIC("e1807", WARN, arangodb::Logger::V8) << "giving up waiting for unused V8 context after " << Logger::FIXED(waitTime) << " s"; }, 60); V8Context* context = nullptr; // look for a free context { CONDITION_LOCKER(guard, _contextCondition); while (_idleContexts.empty() && !_stopping) { TRI_ASSERT(guard.isLocked()); LOG_TOPIC("619ab", TRACE, arangodb::Logger::V8) << "waiting for unused V8 context"; if (!_dirtyContexts.empty()) { // we'll use a dirty context in this case _idleContexts.push_back(_dirtyContexts.back()); _dirtyContexts.pop_back(); break; } bool const contextLimitNotExceeded = (_contexts.size() + _nrInflightContexts < _nrMaxContexts); if (contextLimitNotExceeded && _dynamicContextCreationBlockers == 0) { ++_nrInflightContexts; TRI_ASSERT(guard.isLocked()); guard.unlock(); try { LOG_TOPIC("973d7", DEBUG, Logger::V8) << "creating additional V8 context"; context = addContext(); } catch (...) { guard.lock(); // clean up state --_nrInflightContexts; throw; } // must re-lock TRI_ASSERT(!guard.isLocked()); guard.lock(); --_nrInflightContexts; try { _contexts.push_back(context); } catch (...) { // oops delete context; context = nullptr; continue; } TRI_ASSERT(guard.isLocked()); try { _idleContexts.push_back(context); LOG_TOPIC("25f94", DEBUG, Logger::V8) << "created additional V8 context #" << context->id() << ", number of contexts is now " << _contexts.size(); } catch (...) { TRI_ASSERT(!_contexts.empty()); _contexts.pop_back(); TRI_ASSERT(context != nullptr); delete context; } guard.broadcast(); continue; } TRI_ASSERT(guard.isLocked()); guard.wait(100000); if (exitWhenNoContext.tick()) { vocbase->release(); return nullptr; } } TRI_ASSERT(guard.isLocked()); // in case we are in the shutdown phase, do not enter a context! // the context might have been deleted by the shutdown if (_stopping) { vocbase->release(); return nullptr; } TRI_ASSERT(!_idleContexts.empty()); context = _idleContexts.back(); LOG_TOPIC("bbe93", TRACE, arangodb::Logger::V8) << "found unused V8 context #" << context->id(); TRI_ASSERT(context != nullptr); _idleContexts.pop_back(); // should not fail because we reserved enough space beforehand _busyContexts.emplace(context); } TRI_ASSERT(context != nullptr); context->lockAndEnter(); context->assertLocked(); prepareLockedContext(vocbase, context, securityContext); return context; } void V8DealerFeature::exitContextInternal(V8Context* context) { TRI_DEFER(context->unlockAndExit()); cleanupLockedContext(context); } void V8DealerFeature::cleanupLockedContext(V8Context* context) { TRI_ASSERT(context != nullptr); LOG_TOPIC("e1c52", TRACE, arangodb::Logger::V8) << "leaving V8 context #" << context->id(); auto isolate = context->_isolate; TRI_ASSERT(isolate != nullptr); TRI_GET_GLOBALS(); context->assertLocked(); bool canceled = false; if (V8PlatformFeature::isOutOfMemory(isolate)) { static double const availableTime = 300.0; v8::HandleScope scope(isolate); { auto localContext = v8::Local::New(isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); v8g->_inForcedCollect = true; TRI_RunGarbageCollectionV8(isolate, availableTime); v8g->_inForcedCollect = false; } // needs to be reset after the garbage collection V8PlatformFeature::resetOutOfMemory(isolate); localContext->Exit(); } } // update data for later garbage collection { TRI_GET_GLOBALS(); context->_hasActiveExternals = v8g->hasActiveExternals(); TRI_vocbase_t* vocbase = v8g->_vocbase; TRI_ASSERT(vocbase != nullptr); // release last recently used vocbase vocbase->release(); // check for cancelation requests canceled = v8g->_canceled; v8g->_canceled = false; } // check if we need to execute global context methods bool const runGlobal = context->hasGlobalMethodsQueued(); { v8::HandleScope scope(isolate); // if the execution was canceled, we need to cleanup if (canceled) { context->handleCancelationCleanup(); } // run global context methods if (runGlobal) { context->assertLocked(); try { context->handleGlobalContextMethods(); } catch (...) { // ignore errors here } } TRI_GET_GLOBALS(); // reset the context data; gc should be able to run without it v8g->_vocbase = nullptr; v8g->_securityContext.reset(); // now really exit auto localContext = v8::Local::New(isolate, context->_context); localContext->Exit(); } } void V8DealerFeature::exitContext(V8Context* context) { cleanupLockedContext(context); V8GcThread* gc = static_cast(_gcThread.get()); if (gc != nullptr) { // default is no garbage collection bool performGarbageCollection = false; bool forceGarbageCollection = false; // postpone garbage collection for standard contexts double lastGc = gc->getLastGcStamp(); if (context->_lastGcStamp + _gcFrequency < lastGc) { performGarbageCollection = true; if (context->_lastGcStamp + 30 * _gcFrequency < lastGc) { // force the GC, so that it happens eventually forceGarbageCollection = true; LOG_TOPIC("f543a", TRACE, arangodb::Logger::V8) << "V8 context #" << context->id() << " has reached GC timeout threshold and will be forced into GC"; } else { LOG_TOPIC("f3526", TRACE, arangodb::Logger::V8) << "V8 context #" << context->id() << " has reached GC timeout threshold and will be scheduled for GC"; } } else if (context->invocationsSinceLastGc() >= _gcInterval) { LOG_TOPIC("c6441", TRACE, arangodb::Logger::V8) << "V8 context #" << context->id() << " has reached maximum number of requests and will " "be scheduled for GC"; performGarbageCollection = true; } context->unlockAndExit(); CONDITION_LOCKER(guard, _contextCondition); if (performGarbageCollection && (forceGarbageCollection || !_idleContexts.empty())) { // only add the context to the dirty list if there is at least one other // free context // note that re-adding the context here should not fail as we reserved // enough room for all contexts during startup _dirtyContexts.emplace_back(context); } else { // note that re-adding the context here should not fail as we reserved // enough room for all contexts during startup _idleContexts.emplace_back(context); } _busyContexts.erase(context); LOG_TOPIC("fc763", TRACE, arangodb::Logger::V8) << "returned dirty V8 context #" << context->id(); guard.broadcast(); } else { context->unlockAndExit(); CONDITION_LOCKER(guard, _contextCondition); _busyContexts.erase(context); // note that re-adding the context here should not fail as we reserved // enough room for all contexts during startup _idleContexts.emplace_back(context); LOG_TOPIC("82410", TRACE, arangodb::Logger::V8) << "returned dirty V8 context #" << context->id() << " back into free"; guard.broadcast(); } } void V8DealerFeature::defineContextUpdate( std::function, size_t)> func, TRI_vocbase_t* vocbase) { _contextUpdates.emplace_back(func, vocbase); } // apply context update is only run on contexts that no other // threads can see (yet) void V8DealerFeature::applyContextUpdate(V8Context* context) { for (auto& p : _contextUpdates) { auto vocbase = p.second; if (vocbase == nullptr) { vocbase = server().hasFeature() ? server().getFeature().use().get() : nullptr; if (!vocbase) { continue; } } if (!vocbase->use()) { // oops continue; } JavaScriptSecurityContext securityContext = JavaScriptSecurityContext::createInternalContext(); context->lockAndEnter(); prepareLockedContext(vocbase, context, securityContext); TRI_DEFER(exitContextInternal(context)); { v8::HandleScope scope(context->_isolate); auto localContext = v8::Local::New(context->_isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); p.first(context->_isolate, localContext, context->id()); } localContext->Exit(); } LOG_TOPIC("73e16", TRACE, arangodb::Logger::V8) << "updated V8 context #" << context->id(); } } void V8DealerFeature::shutdownContexts() { _stopping = true; // wait for all contexts to finish { CONDITION_LOCKER(guard, _contextCondition); guard.broadcast(); for (size_t n = 0; n < 10 * 5; ++n) { if (_busyContexts.empty()) { LOG_TOPIC("36259", DEBUG, arangodb::Logger::V8) << "no busy V8 contexts"; break; } LOG_TOPIC("ea785", DEBUG, arangodb::Logger::V8) << "waiting for busy V8 contexts (" << _busyContexts.size() << ") to finish "; guard.wait(100 * 1000); } } // send all busy contexts a terminate signal { CONDITION_LOCKER(guard, _contextCondition); for (auto& it : _busyContexts) { LOG_TOPIC("e907b", WARN, arangodb::Logger::V8) << "sending termination signal to V8 context #" << it->id(); it->_isolate->TerminateExecution(); } } // wait for one minute { CONDITION_LOCKER(guard, _contextCondition); for (size_t n = 0; n < 10 * 60; ++n) { if (_busyContexts.empty()) { break; } guard.wait(100000); } } if (!_busyContexts.empty()) { LOG_TOPIC("4b09f", FATAL, arangodb::Logger::V8) << "cannot shutdown V8 contexts"; FATAL_ERROR_EXIT(); } // stop GC thread if (_gcThread != nullptr) { LOG_TOPIC("c6543", DEBUG, arangodb::Logger::V8) << "waiting for V8 GC thread to finish action"; _gcThread->beginShutdown(); // wait until garbage collector thread is done while (!_gcFinished) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } LOG_TOPIC("ea409", DEBUG, arangodb::Logger::V8) << "commanding V8 GC thread to terminate"; } // shutdown all instances { std::vector contexts; { CONDITION_LOCKER(guard, _contextCondition); contexts = _contexts; _contexts.clear(); } for (auto& context : contexts) { shutdownContext(context); } } LOG_TOPIC("7cdb2", DEBUG, arangodb::Logger::V8) << "V8 contexts are shut down"; } V8Context* V8DealerFeature::pickFreeContextForGc() { int const n = static_cast(_idleContexts.size()); if (n == 0) { // this is easy... return nullptr; } V8GcThread* gc = static_cast(_gcThread.get()); TRI_ASSERT(gc != nullptr); // we got more than 1 context to clean up, pick the one with the "oldest" GC // stamp int pickedContextNr = -1; // index of context with lowest GC stamp, -1 means "none" for (int i = n - 1; i > 0; --i) { // check if there's actually anything to clean up in the context if (_idleContexts[i]->invocationsSinceLastGc() < 50 && !_idleContexts[i]->_hasActiveExternals) { continue; } // compare last GC stamp if (pickedContextNr == -1 || _idleContexts[i]->_lastGcStamp <= _idleContexts[pickedContextNr]->_lastGcStamp) { pickedContextNr = i; } } // we now have the context to clean up in pickedContextNr if (pickedContextNr == -1) { // no context found return nullptr; } // this is the context to clean up V8Context* context = _idleContexts[pickedContextNr]; TRI_ASSERT(context != nullptr); // now compare its last GC timestamp with the last global GC stamp if (context->_lastGcStamp + _gcFrequency >= gc->getLastGcStamp()) { // no need yet to clean up the context return nullptr; } // we'll pop the context from the vector. the context might be at // any position in the vector so we need to move the other elements // around if (n > 1) { for (int i = pickedContextNr; i < n - 1; ++i) { _idleContexts[i] = _idleContexts[i + 1]; } } _idleContexts.pop_back(); return context; } V8Context* V8DealerFeature::buildContext(size_t id) { V8PlatformFeature& v8platform = server().getFeature(); // create isolate v8::Isolate* isolate = v8platform.createIsolate(); TRI_ASSERT(isolate != nullptr); // pass isolate to a new context auto context = std::make_unique(id, isolate); try { // this guard will lock and enter the isolate // and automatically exit and unlock it when it runs out of scope V8ContextEntryGuard contextGuard(context.get()); v8::HandleScope scope(isolate); v8::Handle global = v8::ObjectTemplate::New(isolate); v8::Persistent persistentContext; persistentContext.Reset(isolate, v8::Context::New(isolate, nullptr, global)); auto localContext = v8::Local::New(isolate, persistentContext); localContext->Enter(); { v8::Context::Scope contextScope(localContext); TRI_CreateV8Globals(isolate, id); context->_context.Reset(context->_isolate, localContext); if (context->_context.IsEmpty()) { LOG_TOPIC("ba904", FATAL, arangodb::Logger::V8) << "cannot initialize V8 engine"; FATAL_ERROR_EXIT(); } v8::Handle globalObj = localContext->Global(); globalObj->Set(TRI_V8_ASCII_STRING(isolate, "GLOBAL"), globalObj); globalObj->Set(TRI_V8_ASCII_STRING(isolate, "global"), globalObj); globalObj->Set(TRI_V8_ASCII_STRING(isolate, "root"), globalObj); std::string modules = ""; std::string sep = ""; std::vector directories; directories.insert(directories.end(), _moduleDirectories.begin(), _moduleDirectories.end()); directories.emplace_back(_startupDirectory); if (!_nodeModulesDirectory.empty() && _nodeModulesDirectory != _startupDirectory) { directories.emplace_back(_nodeModulesDirectory); } for (auto const& directory : directories) { modules += sep; sep = ";"; modules += FileUtils::buildFilename(directory, "server/modules") + sep + FileUtils::buildFilename(directory, "common/modules") + sep + FileUtils::buildFilename(directory, "node"); } TRI_InitV8UserFunctions(isolate, localContext); TRI_InitV8UserStructures(isolate, localContext); TRI_InitV8Buffer(isolate); TRI_InitV8Utils(isolate, localContext, _startupDirectory, modules); TRI_InitV8ServerUtils(isolate); TRI_InitV8Shell(isolate); TRI_InitV8Ttl(isolate); { v8::HandleScope scope(isolate); TRI_AddGlobalVariableVocbase(isolate, TRI_V8_ASCII_STRING(isolate, "APP_PATH"), TRI_V8_STD_STRING(isolate, _appPath)); for (auto j : _definedBooleans) { localContext->Global() ->DefineOwnProperty(TRI_IGETC, TRI_V8_STD_STRING(isolate, j.first), v8::Boolean::New(isolate, j.second), v8::ReadOnly) .FromMaybe(false); // Ignore it... } for (auto j : _definedDoubles) { localContext->Global() ->DefineOwnProperty(TRI_IGETC, TRI_V8_STD_STRING(isolate, j.first), v8::Number::New(isolate, j.second), v8::ReadOnly) .FromMaybe(false); // Ignore it... } for (auto const& j : _definedStrings) { localContext->Global() ->DefineOwnProperty(TRI_IGETC, TRI_V8_STD_STRING(isolate, j.first), TRI_V8_STD_STRING(isolate, j.second), v8::ReadOnly) .FromMaybe(false); // Ignore it... } } } // and return from the context localContext->Exit(); } catch (...) { LOG_TOPIC("35586", WARN, Logger::V8) << "caught exception during context initialization"; v8platform.disposeIsolate(isolate); throw; } // some random delay value to add as an initial garbage collection offset // this avoids collecting all contexts at the very same time double const randomWait = static_cast(RandomGenerator::interval(0, 60)); // initialize garbage collection for context context->_hasActiveExternals = true; context->_lastGcStamp = TRI_microtime() + randomWait; LOG_TOPIC("83428", TRACE, arangodb::Logger::V8) << "initialized V8 context #" << id; return context.release(); } V8DealerFeature::Statistics V8DealerFeature::getCurrentContextNumbers() { CONDITION_LOCKER(guard, _contextCondition); return {_contexts.size(), _busyContexts.size(), _dirtyContexts.size(), _idleContexts.size(), _nrMaxContexts}; } std::vector V8DealerFeature::getCurrentMemoryNumbers() { std::vector result; { CONDITION_LOCKER(guard, _contextCondition); result.reserve(_contexts.size()); for (auto oneCtx : _contexts) { auto isolate = oneCtx->_isolate; TRI_GET_GLOBALS(); result.push_back(MemoryStatistics { v8g->_id, v8g->_lastMaxTime, v8g->_countOfTimes, v8g->_heapMax, v8g->_heapLow }); } } return result; } bool V8DealerFeature::loadJavaScriptFileInContext(TRI_vocbase_t* vocbase, std::string const& file, V8Context* context, VPackBuilder* builder) { TRI_ASSERT(vocbase != nullptr); TRI_ASSERT(context != nullptr); if (_stopping) { return false; } if (!vocbase->use()) { return false; } JavaScriptSecurityContext securityContext = JavaScriptSecurityContext::createInternalContext(); context->lockAndEnter(); prepareLockedContext(vocbase, context, securityContext); TRI_DEFER(exitContextInternal(context)); try { loadJavaScriptFileInternal(file, context, builder); } catch (...) { LOG_TOPIC("e099e", WARN, Logger::V8) << "caught exception while executing JavaScript file '" << file << "' in context #" << context->id(); throw; } return true; } void V8DealerFeature::loadJavaScriptFileInternal(std::string const& file, V8Context* context, VPackBuilder* builder) { v8::HandleScope scope(context->_isolate); auto localContext = v8::Local::New(context->_isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); switch (_startupLoader.loadScript(context->_isolate, localContext, file, builder)) { case JSLoader::eSuccess: LOG_TOPIC("29e73", TRACE, arangodb::Logger::V8) << "loaded JavaScript file '" << file << "'"; break; case JSLoader::eFailLoad: LOG_TOPIC("0f13b", FATAL, arangodb::Logger::V8) << "cannot load JavaScript file '" << file << "'"; FATAL_ERROR_EXIT(); break; case JSLoader::eFailExecute: LOG_TOPIC("69ac3", FATAL, arangodb::Logger::V8) << "error during execution of JavaScript file '" << file << "'"; FATAL_ERROR_EXIT(); break; } } localContext->Exit(); LOG_TOPIC("53bbb", TRACE, arangodb::Logger::V8) << "loaded Javascript file '" << file << "' for V8 context #" << context->id(); } void V8DealerFeature::shutdownContext(V8Context* context) { TRI_ASSERT(context != nullptr); LOG_TOPIC("7946e", TRACE, arangodb::Logger::V8) << "shutting down V8 context #" << context->id(); auto isolate = context->_isolate; { // this guard will lock and enter the isolate // and automatically exit and unlock it when it runs out of scope V8ContextEntryGuard contextGuard(context); v8::HandleScope scope(isolate); TRI_GET_GLOBALS(); auto localContext = v8::Local::New(isolate, context->_context); localContext->Enter(); { v8::Context::Scope contextScope(localContext); TRI_VisitActions( [&isolate](TRI_action_t* action) { action->visit(isolate); }); v8g->_inForcedCollect = true; TRI_RunGarbageCollectionV8(isolate, 30.0); v8g->_inForcedCollect = false; TRI_GET_GLOBALS(); if (v8g != nullptr) { if (v8g->_transactionContext != nullptr) { delete static_cast(v8g->_transactionContext); v8g->_transactionContext = nullptr; } delete v8g; } } localContext->Exit(); } context->_context.Reset(); server().getFeature().disposeIsolate(isolate); LOG_TOPIC("34c28", TRACE, arangodb::Logger::V8) << "closed V8 context #" << context->id(); delete context; } V8ContextGuard::V8ContextGuard(TRI_vocbase_t* vocbase, JavaScriptSecurityContext const& securityContext) : _isolate(nullptr), _context(nullptr) { _context = V8DealerFeature::DEALER->enterContext(vocbase, securityContext); if (_context == nullptr) { THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_RESOURCE_LIMIT, "unable to acquire V8 context in time"); } _isolate = _context->_isolate; } V8ContextGuard::~V8ContextGuard() { if (_context) { try { V8DealerFeature::DEALER->exitContext(_context); } catch (...) { } } } V8ConditionalContextGuard::V8ConditionalContextGuard(Result& res, v8::Isolate*& isolate, TRI_vocbase_t* vocbase, JavaScriptSecurityContext const& securityContext) : _isolate(isolate), _context(nullptr), _active(isolate ? false : true) { if (_active) { if (!vocbase) { res.reset(TRI_ERROR_INTERNAL, "V8ConditionalContextGuard - no vocbase provided"); return; } _context = V8DealerFeature::DEALER->enterContext(vocbase, securityContext); if (!_context) { res.reset(TRI_ERROR_INTERNAL, "V8ConditionalContextGuard - could not acquire context"); return; } isolate = _context->_isolate; } } V8ConditionalContextGuard::~V8ConditionalContextGuard() { if (_active && _context) { try { V8DealerFeature::DEALER->exitContext(_context); } catch (...) { } _isolate = nullptr; } }