mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 10:45:11 +08:00
Putting a time limit on a Node.js process is common in many
use cases, but each of the common approaches are generally
flawed.
OS and CI timeouts are inconsistent and blind. Every OS flavor
handles process timeouts in different ways. Stock macOS and
Windows do not ship coreutils `timeout`, etc. None of the options
can explain why the process was still running or what exactly
was interrupted. Using setTimeout only fires when the event loop
is free so it misses busy loops and thread blocks and cannot
see the stack. Diagnostics triggered by signal have the same
problem.
This introduces a `--process-timeout` that should work
consistently across all runtimes. A native watchdog thread
interrupts the main thread, even in the middle of running
code. It prints the JavaScript stack (if any) and resources
keeping the event loop alive (if any), and can optionally
print a full diagnostic report. The process exits with a
distinct code (124). If the main thread is stuck in native
code, the process still exits.
$ node --process-timeout=10s -e "while(true) {}"
(node:483970) Process timed out after 10s (--process-timeout). Exiting with code 124.
Main thread was executing JavaScript:
at [eval]:1:1
at runScriptInThisContext (node:internal/vm:219:10)
at node:internal/process/execution:485:12
at [eval]-wrapper:6:24
at runScriptInContext (node:internal/process/execution:483:60)
at evalFunction (node:internal/process/execution:317:30)
at evalTypeScript (node:internal/process/execution:329:3)
at node:internal/main/eval_string:71:3
No resources keeping the event loop alive were found.
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
PR-URL: https://github.com/nodejs/node/pull/66138
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
168 lines
5.8 KiB
C++
168 lines
5.8 KiB
C++
#ifndef SRC_NODE_WORKER_H_
|
|
#define SRC_NODE_WORKER_H_
|
|
|
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
|
|
|
#include <optional>
|
|
#include <unordered_map>
|
|
#include "json_utils.h"
|
|
#include "node_exit_code.h"
|
|
#include "node_messaging.h"
|
|
#include "util.h"
|
|
#include "uv.h"
|
|
|
|
namespace node {
|
|
|
|
struct SnapshotData;
|
|
namespace worker {
|
|
|
|
class WorkerThreadData;
|
|
|
|
enum ResourceLimits {
|
|
kMaxYoungGenerationSizeMb,
|
|
kMaxOldGenerationSizeMb,
|
|
kCodeRangeSizeMb,
|
|
kStackSizeMb,
|
|
kTotalResourceLimitCount
|
|
};
|
|
|
|
// A worker thread, as represented in its parent thread.
|
|
class Worker : public AsyncWrap {
|
|
public:
|
|
Worker(Environment* env,
|
|
v8::Local<v8::Object> wrap,
|
|
const std::string& url,
|
|
const std::string& name,
|
|
std::shared_ptr<PerIsolateOptions> per_isolate_opts,
|
|
std::vector<std::string>&& exec_argv,
|
|
std::shared_ptr<KVStore> env_vars,
|
|
const SnapshotData* snapshot_data,
|
|
const bool is_internal);
|
|
~Worker() override;
|
|
|
|
// Run the worker. This is only called from the worker thread.
|
|
void Run();
|
|
bool UseWorkerContextSnapshot() const;
|
|
|
|
// Forcibly exit the thread with a specified exit code. This may be called
|
|
// from any thread. `error_code` and `error_message` can be used to create
|
|
// a custom `'error'` event before emitting `'exit'`.
|
|
void Exit(ExitCode code,
|
|
const char* error_code = nullptr,
|
|
const char* error_message = nullptr);
|
|
|
|
// Wait for the worker thread to stop (in a blocking manner).
|
|
void JoinThread();
|
|
|
|
template <typename Fn>
|
|
inline bool RequestInterrupt(Fn&& cb);
|
|
|
|
SET_NO_MEMORY_INFO()
|
|
SET_MEMORY_INFO_NAME(Worker)
|
|
SET_SELF_SIZE(Worker)
|
|
bool IsNotIndicativeOfMemoryLeakAtExit() const override;
|
|
|
|
bool is_stopped() const;
|
|
const SnapshotData* snapshot_data() const { return snapshot_data_; }
|
|
bool is_internal() const { return is_internal_; }
|
|
std::string_view name() const { return name_; }
|
|
uint64_t thread_id() const { return thread_id_.id; }
|
|
// Whether the Worker keeps the parent's event loop alive, see
|
|
// `worker.ref()` and `worker.unref()`. Only valid on the parent thread.
|
|
bool has_ref() const { return has_ref_; }
|
|
|
|
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StartThread(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StopThread(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void HasRef(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void Ref(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void Unref(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void GetResourceLimits(
|
|
const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
v8::Local<v8::Float64Array> GetResourceLimits(v8::Isolate* isolate) const;
|
|
static void TakeHeapSnapshot(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void LoopIdleTime(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void LoopStartTime(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void GetHeapStatistics(
|
|
const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void CpuUsage(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StartCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StopCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StartHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
static void StopHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
|
|
|
|
private:
|
|
bool CreateEnvMessagePort(Environment* env);
|
|
static size_t NearHeapLimit(void* data, size_t current_heap_limit,
|
|
size_t initial_heap_limit);
|
|
|
|
std::shared_ptr<PerIsolateOptions> per_isolate_opts_;
|
|
std::vector<std::string> exec_argv_;
|
|
std::vector<std::string> argv_;
|
|
|
|
MultiIsolatePlatform* platform_;
|
|
v8::Isolate* isolate_ = nullptr;
|
|
std::optional<uv_thread_t> tid_; // Set while the thread is running
|
|
|
|
std::unique_ptr<InspectorParentHandle> inspector_parent_handle_;
|
|
|
|
// This mutex protects access to all variables listed below it.
|
|
mutable Mutex mutex_;
|
|
|
|
const char* custom_error_ = nullptr;
|
|
std::string custom_error_str_;
|
|
ExitCode exit_code_ = ExitCode::kNoFailure;
|
|
ThreadId thread_id_;
|
|
uintptr_t stack_base_ = 0;
|
|
// Optional name used for debugging in inspector and trace events.
|
|
std::string name_;
|
|
|
|
// Custom resource constraints:
|
|
double resource_limits_[kTotalResourceLimitCount];
|
|
void UpdateResourceConstraints(v8::ResourceConstraints* constraints);
|
|
|
|
// Full size of the thread's stack.
|
|
size_t stack_size_ = 4 * 1024 * 1024;
|
|
// Stack buffer size that is not available to the JS engine.
|
|
static constexpr size_t kStackBufferSize = 192 * 1024;
|
|
|
|
std::unique_ptr<MessagePortData> child_port_data_;
|
|
std::shared_ptr<KVStore> env_vars_;
|
|
EmbedderPreloadCallback embedder_preload_;
|
|
|
|
// A raw flag that is used by creator and worker threads to
|
|
// sync up on pre-mature termination of worker - while in the
|
|
// warmup phase. Once the worker is fully warmed up, use the
|
|
// async handle of the worker's Environment for the same purpose.
|
|
bool stopped_ = true;
|
|
|
|
bool has_ref_ = true;
|
|
uint64_t environment_flags_ = EnvironmentFlags::kNoFlags;
|
|
|
|
// The real Environment of the worker object. It has a lesser
|
|
// lifespan than the worker object itself - comes to life
|
|
// when the worker thread creates a new Environment, and gets
|
|
// destroyed alongwith the worker thread.
|
|
Environment* env_ = nullptr;
|
|
|
|
const SnapshotData* snapshot_data_ = nullptr;
|
|
const bool is_internal_;
|
|
friend class WorkerThreadData;
|
|
};
|
|
|
|
template <typename Fn>
|
|
bool Worker::RequestInterrupt(Fn&& cb) {
|
|
Mutex::ScopedLock lock(mutex_);
|
|
if (env_ == nullptr) return false;
|
|
env_->RequestInterrupt(std::move(cb));
|
|
return true;
|
|
}
|
|
|
|
} // namespace worker
|
|
} // namespace node
|
|
|
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
|
|
|
|
|
#endif // SRC_NODE_WORKER_H_
|