mirror of
https://github.com/matrix-construct/construct
synced 2024-12-26 15:33:54 +01:00
ircd::js: Add wait-free execution interruption.
This commit is contained in:
parent
cf71fd5c5e
commit
ad1fef4247
3 changed files with 366 additions and 157 deletions
|
@ -25,34 +25,66 @@
|
||||||
namespace ircd {
|
namespace ircd {
|
||||||
namespace js {
|
namespace js {
|
||||||
|
|
||||||
|
// Indicates the phase of execution of the javascript
|
||||||
|
enum class phase
|
||||||
|
:uint8_t
|
||||||
|
{
|
||||||
|
ACCEPT, // JS is not running.
|
||||||
|
ENTER, // JS is currently executing or is committed to being entered.
|
||||||
|
INTR, // An interrupt request has or is committed to being sent.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicates what operation the interrupt is for
|
||||||
|
enum class irq
|
||||||
|
:uint8_t
|
||||||
|
{
|
||||||
|
NONE, // Sentinel value (no interrupt) (spurious)
|
||||||
|
JS, // JS itself triggers an interrupt after data init before code exec.
|
||||||
|
USER, // User interrupts to have handler (on_intr) called.
|
||||||
|
YIELD, // An ircd::ctx yield should take place, then javascript continues.
|
||||||
|
TERMINATE, // The javascript should be terminated.
|
||||||
|
};
|
||||||
|
|
||||||
struct context
|
struct context
|
||||||
:private custom_ptr<JSContext>
|
:private custom_ptr<JSContext>
|
||||||
{
|
{
|
||||||
class lock
|
using steady_clock = std::chrono::steady_clock;
|
||||||
{
|
using time_point = steady_clock::time_point;
|
||||||
context *c;
|
|
||||||
|
|
||||||
public:
|
struct alignas(8) state
|
||||||
lock(context &c); // BeginRequest on cx of your choice
|
{
|
||||||
lock(); // BeginRequest on the thread_local cx
|
uint32_t sem;
|
||||||
~lock() noexcept; // EndRequest
|
enum phase phase;
|
||||||
|
enum irq irq;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct opts
|
struct opts
|
||||||
{
|
{
|
||||||
size_t stack_chunk_size = 8_KiB;
|
size_t stack_chunk_size = 8_KiB;
|
||||||
bool dtor_gc = true;
|
microseconds timer_interval = 250000us;
|
||||||
|
microseconds timer_limit = 500000us;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct exstate
|
struct opts opts;
|
||||||
{
|
|
||||||
JSExceptionState *state; // exstate
|
|
||||||
JSErrorReport report; // note: ptrs within are not necessarily safe
|
|
||||||
};
|
|
||||||
|
|
||||||
struct opts opts; // We keep a copy of the given opts here.
|
// Interruption state
|
||||||
std::stack<struct exstate> exstate;
|
std::atomic<struct state> state; // Atomic state of execution
|
||||||
|
std::function<int (const irq &)> on_intr; // User interrupt hook (ret -1 to not interfere)
|
||||||
|
|
||||||
|
// Exception state
|
||||||
|
JSExceptionState *except; // Use save_exception()/restore_exception()
|
||||||
|
JSErrorReport report; // Note: ptrs may not be valid in here.
|
||||||
|
|
||||||
|
// Execution timer
|
||||||
|
microseconds limit; // The maximum duration for an execution
|
||||||
|
steady_clock::time_point started; // When the execution started
|
||||||
|
std::atomic<microseconds> interval; // The interval the timer will check the above
|
||||||
|
bool handle_interrupt(); // Called by runtime on interrupt
|
||||||
|
void timer_check();
|
||||||
|
void timer_worker();
|
||||||
|
std::thread timer;
|
||||||
|
|
||||||
|
// JSContext
|
||||||
operator JSContext *() const { return get(); }
|
operator JSContext *() const { return get(); }
|
||||||
operator JSContext &() const { return custom_ptr<JSContext>::operator*(); }
|
operator JSContext &() const { return custom_ptr<JSContext>::operator*(); }
|
||||||
bool operator!() const { return !custom_ptr<JSContext>::operator bool(); }
|
bool operator!() const { return !custom_ptr<JSContext>::operator bool(); }
|
||||||
|
@ -61,12 +93,14 @@ struct context
|
||||||
auto ptr() const { return get(); }
|
auto ptr() const { return get(); }
|
||||||
auto ptr() { return get(); }
|
auto ptr() { return get(); }
|
||||||
|
|
||||||
|
// BasicLockable // std::lock_guard<context>
|
||||||
|
void lock() { JS_BeginRequest(get()); }
|
||||||
|
void unlock() { JS_EndRequest(get()); }
|
||||||
|
|
||||||
context(JSRuntime *const &, const struct opts &);
|
context(JSRuntime *const &, const struct opts &);
|
||||||
context() = default;
|
context() = default;
|
||||||
context(context &&) noexcept;
|
context(context &&) = delete;
|
||||||
context(const context &) = delete;
|
context(const context &) = delete;
|
||||||
context &operator=(context &&) noexcept;
|
|
||||||
context &operator=(const context &) = delete;
|
|
||||||
~context() noexcept;
|
~context() noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,10 +109,6 @@ struct context
|
||||||
// Do not construct more than one context on the same thread- this is overwritten.
|
// Do not construct more than one context on the same thread- this is overwritten.
|
||||||
extern __thread context *cx;
|
extern __thread context *cx;
|
||||||
|
|
||||||
// A default JSContext instance is provided residing near the main runtime as a convenience
|
|
||||||
// for misc/utility/system purposes if necessary. You should use *cx instead.
|
|
||||||
extern context main_cx;
|
|
||||||
|
|
||||||
// Get to our `struct context` from any upstream JSContext
|
// Get to our `struct context` from any upstream JSContext
|
||||||
const context &our(const JSContext *const &);
|
const context &our(const JSContext *const &);
|
||||||
context &our(JSContext *const &);
|
context &our(JSContext *const &);
|
||||||
|
@ -88,21 +118,67 @@ template<class T = privdata> const T *priv(const context &);
|
||||||
template<class T = privdata> T *priv(context &);
|
template<class T = privdata> T *priv(context &);
|
||||||
void priv(context &, privdata *const &);
|
void priv(context &, privdata *const &);
|
||||||
|
|
||||||
inline auto version(const context &c) { return version(JS_GetVersion(c)); }
|
// Misc
|
||||||
inline auto running(const context &c) { return JS_IsRunning(c); }
|
inline auto running(const context &c) { return JS_IsRunning(c); }
|
||||||
inline auto uncaught_exception(const context &c) { return JS_IsExceptionPending(c); }
|
inline auto version(const context &c) { return version(JS_GetVersion(c)); }
|
||||||
inline auto interrupted(const context &c) { return JS_CheckForInterrupt(c); }
|
JSObject *current_global(context &c);
|
||||||
|
JSObject *current_global(); // thread_local
|
||||||
|
|
||||||
|
// Memory
|
||||||
inline void out_of_memory(context &c) { JS_ReportOutOfMemory(c); }
|
inline void out_of_memory(context &c) { JS_ReportOutOfMemory(c); }
|
||||||
inline void allocation_overflow(context &c) { JS_ReportAllocationOverflow(c); }
|
inline void allocation_overflow(context &c) { JS_ReportAllocationOverflow(c); }
|
||||||
inline void run_gc(context &c) { JS_MaybeGC(c); }
|
inline void run_gc(context &c) { JS_MaybeGC(c); }
|
||||||
JSObject *current_global(context &c);
|
|
||||||
bool rethrow_exception(context &c);
|
|
||||||
void push_exception(context &c, const JSErrorReport &);
|
|
||||||
JSErrorReport pop_exception(context &c);
|
|
||||||
|
|
||||||
// thread_local
|
// Exception
|
||||||
JSObject *current_global();
|
bool pending_exception(const context &c);
|
||||||
|
void save_exception(context &c, const JSErrorReport &);
|
||||||
|
bool restore_exception(context &c);
|
||||||
|
bool report_exception(context &c);
|
||||||
|
|
||||||
|
// Interruption
|
||||||
|
// The interruption has to occur transactionally so you send a condition to the interruptor
|
||||||
|
// via a synchronous closure. Returns true when committed to interrupt.
|
||||||
|
//
|
||||||
|
// * If JS is not ready for interruption the condition is ignored.
|
||||||
|
// * If it is ready but your condition fails (returns irq::NONE) then no interrupt.
|
||||||
|
// * If the condition returns an irq but JS is no longer ready you are declined.
|
||||||
|
using interrupt_condition = std::function<irq ()>;
|
||||||
|
bool interrupt(context &, const interrupt_condition &);
|
||||||
|
bool interrupt_poll(const context &c);
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
void enter(context &); // throws if can't enter
|
||||||
|
void leave(context &); // must be called if enter() succeeds
|
||||||
|
|
||||||
|
// Enter JS within this closure. Most likely your function will return a `struct value`
|
||||||
|
// or JS::Value returned by most calls into JS.
|
||||||
|
template<class F> auto run(F&& function);
|
||||||
|
|
||||||
|
|
||||||
|
template<class F>
|
||||||
|
auto
|
||||||
|
run(F&& function)
|
||||||
|
{
|
||||||
|
enter(*cx);
|
||||||
|
const scope out([]
|
||||||
|
{
|
||||||
|
leave(*cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return function();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool
|
||||||
|
report_exception(context &c)
|
||||||
|
{
|
||||||
|
return JS_ReportPendingException(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool
|
||||||
|
pending_exception(const context &c)
|
||||||
|
{
|
||||||
|
return JS_IsExceptionPending(c);
|
||||||
|
}
|
||||||
|
|
||||||
inline JSObject *
|
inline JSObject *
|
||||||
current_global()
|
current_global()
|
||||||
|
@ -110,12 +186,6 @@ current_global()
|
||||||
return current_global(*cx);
|
return current_global(*cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool
|
|
||||||
rethrow_exception(context &c)
|
|
||||||
{
|
|
||||||
return JS_ReportPendingException(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline JSObject *
|
inline JSObject *
|
||||||
current_global(context &c)
|
current_global(context &c)
|
||||||
{
|
{
|
||||||
|
@ -126,7 +196,8 @@ inline void
|
||||||
priv(context &c,
|
priv(context &c,
|
||||||
privdata *const &ptr)
|
privdata *const &ptr)
|
||||||
{
|
{
|
||||||
delete priv(c); // Free any existing object to overwrite/null
|
// Free any existing object to overwrite/null
|
||||||
|
delete priv(c);
|
||||||
JS_SetSecondContextPrivate(c, ptr);
|
JS_SetSecondContextPrivate(c, ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,3 +229,5 @@ our(const JSContext *const &c)
|
||||||
|
|
||||||
} // namespace js
|
} // namespace js
|
||||||
} // namespace ircd
|
} // namespace ircd
|
||||||
|
|
||||||
|
static_assert(sizeof(struct ircd::js::context::state) == 8, "");
|
||||||
|
|
|
@ -71,10 +71,6 @@ class runtime
|
||||||
// Do not construct more than one runtime on the same thread- this is overwritten.
|
// Do not construct more than one runtime on the same thread- this is overwritten.
|
||||||
extern __thread runtime *rt;
|
extern __thread runtime *rt;
|
||||||
|
|
||||||
// Main JSRuntime instance. It is only valid while the js::init object is held by ircd::main().
|
|
||||||
// This is available to always find main for whatever reason, but use *rt instead.
|
|
||||||
extern runtime main;
|
|
||||||
|
|
||||||
// Get to our `struct runtime` from any upstream JSRuntime
|
// Get to our `struct runtime` from any upstream JSRuntime
|
||||||
const runtime &our(const JSRuntime *const &);
|
const runtime &our(const JSRuntime *const &);
|
||||||
runtime &our(JSRuntime *const &);
|
runtime &our(JSRuntime *const &);
|
||||||
|
@ -83,6 +79,7 @@ runtime &our(JSRuntime *const &);
|
||||||
const runtime &object_runtime(const JSObject &);
|
const runtime &object_runtime(const JSObject &);
|
||||||
runtime &object_runtime(JSObject &);
|
runtime &object_runtime(JSObject &);
|
||||||
|
|
||||||
|
// Do not call interrupt() unless you know what you're doing; see context.h.
|
||||||
void interrupt(runtime &r);
|
void interrupt(runtime &r);
|
||||||
void run_gc(runtime &r);
|
void run_gc(runtime &r);
|
||||||
|
|
||||||
|
@ -96,7 +93,6 @@ run_gc(runtime &r)
|
||||||
inline void
|
inline void
|
||||||
interrupt(runtime &r)
|
interrupt(runtime &r)
|
||||||
{
|
{
|
||||||
JS_SetInterruptCallback(r, runtime::handle_interrupt);
|
|
||||||
JS_RequestInterruptCallback(r);
|
JS_RequestInterruptCallback(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
366
ircd/js.cc
366
ircd/js.cc
|
@ -32,13 +32,9 @@ namespace js {
|
||||||
__thread runtime *rt;
|
__thread runtime *rt;
|
||||||
__thread context *cx;
|
__thread context *cx;
|
||||||
|
|
||||||
// Location of the main JSRuntime instance. An extern reference to this exists in js/runtime.h.
|
// Location of the main JSRuntime and JSContext instances.
|
||||||
// It is null until js::init manually constructs (and later destructs) it.
|
runtime *main_runtime;
|
||||||
runtime main;
|
context *main_context;
|
||||||
|
|
||||||
// Location of the default/main JSContext instance. An extern reference to this exists in js/context.h.
|
|
||||||
// Lifetime similar to main runtime
|
|
||||||
context mc;
|
|
||||||
|
|
||||||
// Logging facility for this submodule with SNOMASK.
|
// Logging facility for this submodule with SNOMASK.
|
||||||
struct log::log log
|
struct log::log log
|
||||||
|
@ -55,12 +51,18 @@ struct log::log log
|
||||||
//
|
//
|
||||||
|
|
||||||
ircd::js::init::init()
|
ircd::js::init::init()
|
||||||
try
|
|
||||||
{
|
{
|
||||||
log.info("Initializing the JS engine [%s: %s]",
|
log.info("Initializing the JS engine [%s: %s]",
|
||||||
"SpiderMonkey",
|
"SpiderMonkey",
|
||||||
version(ver::IMPLEMENTATION));
|
version(ver::IMPLEMENTATION));
|
||||||
|
|
||||||
|
const scope exit([this]
|
||||||
|
{
|
||||||
|
// Ensure ~init() is always safe to call at any intermediate state
|
||||||
|
if(std::current_exception())
|
||||||
|
this->~init();
|
||||||
|
});
|
||||||
|
|
||||||
if(!JS_Init())
|
if(!JS_Init())
|
||||||
throw error("JS_Init(): failure");
|
throw error("JS_Init(): failure");
|
||||||
|
|
||||||
|
@ -69,38 +71,35 @@ try
|
||||||
log.info("Initializing the main JS Runtime (main_maxbytes: %zu)",
|
log.info("Initializing the main JS Runtime (main_maxbytes: %zu)",
|
||||||
runtime_opts.maxbytes);
|
runtime_opts.maxbytes);
|
||||||
|
|
||||||
main = runtime(runtime_opts);
|
main_runtime = new runtime(runtime_opts);
|
||||||
mc = context(main, context_opts);
|
main_context = new context(*main_runtime, context_opts);
|
||||||
log.info("Initialized main JS Runtime and context (version: '%s')",
|
log.info("Initialized main JS Runtime and context (version: '%s')",
|
||||||
version(mc));
|
version(*main_context));
|
||||||
|
|
||||||
{
|
{
|
||||||
context::lock lock;
|
const std::lock_guard<context> lock{*main_context};
|
||||||
ircd::mods::load("kernel");
|
ircd::mods::load("kernel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(...)
|
|
||||||
{
|
|
||||||
this->~init();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::js::init::~init()
|
ircd::js::init::~init()
|
||||||
noexcept
|
noexcept
|
||||||
{
|
{
|
||||||
if(cx && !!*cx)
|
if(main_context && !!*main_context) try
|
||||||
{
|
{
|
||||||
context::lock lock;
|
const std::lock_guard<context> lock{*main_context};
|
||||||
ircd::mods::unload("kernel");
|
ircd::mods::unload("kernel");
|
||||||
}
|
}
|
||||||
|
catch(const std::exception &e)
|
||||||
|
{
|
||||||
|
log.warning("Failed to unload the kernel: %s", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Terminating the main JS Runtime");
|
log.info("Terminating the JS Main Runtime");
|
||||||
|
delete main_context;
|
||||||
|
delete main_runtime;
|
||||||
|
|
||||||
// Assign empty objects to free and reset
|
log.info("Terminating the JS Engine");
|
||||||
mc = context{};
|
|
||||||
main = runtime{};
|
|
||||||
|
|
||||||
log.info("Terminating the JS engine");
|
|
||||||
JS_ShutDown();
|
JS_ShutDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -855,7 +854,10 @@ ircd::js::jserror::jserror(pending_t)
|
||||||
:ircd::js::error{generate_skip}
|
:ircd::js::error{generate_skip}
|
||||||
,val{*cx}
|
,val{*cx}
|
||||||
{
|
{
|
||||||
auto report(pop_exception(*cx));
|
if(!restore_exception(*cx))
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto &report(cx->report);
|
||||||
if(report.flags & JSREPORT_EXCEPTION &&
|
if(report.flags & JSREPORT_EXCEPTION &&
|
||||||
report.errorNumber != 105)
|
report.errorNumber != 105)
|
||||||
{
|
{
|
||||||
|
@ -1161,8 +1163,23 @@ ircd::js::context::context(JSRuntime *const &runtime,
|
||||||
const struct opts &opts)
|
const struct opts &opts)
|
||||||
:custom_ptr<JSContext>
|
:custom_ptr<JSContext>
|
||||||
{
|
{
|
||||||
JS_NewContext(runtime, opts.stack_chunk_size),
|
// Construct the context
|
||||||
[opts](JSContext *const ctx) //TODO: old gcc/clang can't copy from opts.dtor_gc
|
[this, &runtime, &opts]
|
||||||
|
{
|
||||||
|
const auto ret(JS_NewContext(runtime, opts.stack_chunk_size));
|
||||||
|
|
||||||
|
// Use their privdata pointer to point to our instance. We can then use our(JSContext*)
|
||||||
|
// to get back to `this` instance.
|
||||||
|
JS_SetContextPrivate(ret, this);
|
||||||
|
|
||||||
|
// Assign the thread_local here.
|
||||||
|
cx = this;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}(),
|
||||||
|
|
||||||
|
// Plant the destruction
|
||||||
|
[](JSContext *const ctx)
|
||||||
noexcept
|
noexcept
|
||||||
{
|
{
|
||||||
if(!ctx)
|
if(!ctx)
|
||||||
|
@ -1171,111 +1188,233 @@ ircd::js::context::context(JSRuntime *const &runtime,
|
||||||
// Free the user's privdata managed object
|
// Free the user's privdata managed object
|
||||||
delete static_cast<const privdata *>(JS_GetSecondContextPrivate(ctx));
|
delete static_cast<const privdata *>(JS_GetSecondContextPrivate(ctx));
|
||||||
|
|
||||||
if(opts.dtor_gc)
|
JS_DestroyContext(ctx);
|
||||||
JS_DestroyContext(ctx);
|
|
||||||
else
|
// Indicate to thread_local
|
||||||
JS_DestroyContextNoGC(ctx);
|
cx = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
,opts{opts}
|
,opts{opts}
|
||||||
|
,state
|
||||||
|
{{
|
||||||
|
0, // Semaphore value starting at 0.
|
||||||
|
phase::ACCEPT, // ACCEPT phase indicates nothing is running.
|
||||||
|
irq::JS, // irq::JS is otherwise here in case JS triggers an interrupt.
|
||||||
|
}}
|
||||||
|
,except{nullptr}
|
||||||
|
,limit{opts.timer_limit}
|
||||||
|
,started{time_point::min()}
|
||||||
|
,interval{opts.timer_interval}
|
||||||
|
,timer{std::bind(&context::timer_worker, this)}
|
||||||
{
|
{
|
||||||
assert(&our(runtime) == rt); // Trying to construct on thread without runtime thread_local
|
assert(&our(runtime) == rt); // Trying to construct on thread without runtime thread_local
|
||||||
|
|
||||||
// Use their privdata pointer to point to our instance. We can then use our(JSContext*)
|
|
||||||
// to get back to `this` instance. Remember the pointer must change when this class is
|
|
||||||
// std::move()'ed etc via the move constructor/assignment.
|
|
||||||
JS_SetContextPrivate(get(), this);
|
|
||||||
|
|
||||||
// Assign the thread_local here.
|
|
||||||
cx = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::js::context::context(context &&other)
|
|
||||||
noexcept
|
|
||||||
:custom_ptr<JSContext>{std::move(other)}
|
|
||||||
,opts{std::move(other.opts)}
|
|
||||||
,exstate{std::move(other.exstate)}
|
|
||||||
{
|
|
||||||
// Branch not taken for null/defaulted instance of JSContext smart ptr
|
|
||||||
if(!!*this)
|
|
||||||
JS_SetContextPrivate(get(), this);
|
|
||||||
|
|
||||||
// Ensure the thread_local points here now.
|
|
||||||
cx = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
ircd::js::context &
|
|
||||||
ircd::js::context::operator=(context &&other)
|
|
||||||
noexcept
|
|
||||||
{
|
|
||||||
static_cast<custom_ptr<JSContext> &>(*this) = std::move(other);
|
|
||||||
opts = std::move(other.opts);
|
|
||||||
exstate = std::move(other.exstate);
|
|
||||||
|
|
||||||
// Branch not taken for null/defaulted instance of JSContext smart ptr
|
|
||||||
if(!!*this)
|
|
||||||
JS_SetContextPrivate(get(), this);
|
|
||||||
|
|
||||||
// Ensure the thread_local points here now.
|
|
||||||
cx = this;
|
|
||||||
|
|
||||||
return *this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ircd::js::context::~context()
|
ircd::js::context::~context()
|
||||||
noexcept
|
noexcept
|
||||||
{
|
{
|
||||||
// Branch not taken on std::move()
|
interval.store(microseconds(0), std::memory_order_release);
|
||||||
if(!!*this)
|
timer.join();
|
||||||
cx = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
JSErrorReport
|
|
||||||
ircd::js::pop_exception(context &c)
|
|
||||||
{
|
|
||||||
if(unlikely(c.exstate.empty()))
|
|
||||||
throw error("(internal error) No pending exception to restore");
|
|
||||||
|
|
||||||
auto &top(c.exstate.top());
|
|
||||||
JS_RestoreExceptionState(c, top.state);
|
|
||||||
const auto report(top.report);
|
|
||||||
c.exstate.pop();
|
|
||||||
return report;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ircd::js::push_exception(context &c,
|
ircd::js::context::timer_worker()
|
||||||
const JSErrorReport &report)
|
|
||||||
{
|
{
|
||||||
c.exstate.push
|
while(interval.load(std::memory_order_consume) > microseconds(0))
|
||||||
({
|
|
||||||
JS_SaveExceptionState(c),
|
|
||||||
report
|
|
||||||
});
|
|
||||||
|
|
||||||
if(unlikely(!c.exstate.top().state))
|
|
||||||
{
|
{
|
||||||
c.exstate.pop();
|
std::this_thread::sleep_for(interval.load(std::memory_order_consume));
|
||||||
throw error("(internal error) No pending exception to save");
|
timer_check();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
ircd::js::context::lock::lock()
|
ircd::js::context::timer_check()
|
||||||
:lock{*cx}
|
|
||||||
{
|
{
|
||||||
|
// The interruption is an atomic transaction so our condition
|
||||||
|
// for a timer excess occurs in a synchronous closure.
|
||||||
|
|
||||||
|
interrupt(*this, [this]
|
||||||
|
{
|
||||||
|
// See if the execution has timing.
|
||||||
|
if(started == time_point::min() || limit == microseconds(0))
|
||||||
|
return irq::NONE;
|
||||||
|
|
||||||
|
// Check if timer exceeded limits.
|
||||||
|
const auto over_limit(steady_clock::now() - limit > started);
|
||||||
|
if(!over_limit)
|
||||||
|
return irq::NONE;
|
||||||
|
|
||||||
|
// Attempt to kill it.
|
||||||
|
return irq::TERMINATE;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ircd::js::context::lock::lock(context &c)
|
bool
|
||||||
:c{&c}
|
ircd::js::context::handle_interrupt()
|
||||||
{
|
{
|
||||||
JS_BeginRequest(c);
|
auto state(this->state.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
// Spurious interrupt; ignore.
|
||||||
|
if(unlikely(state.phase != phase::INTR && state.phase != phase::ENTER))
|
||||||
|
{
|
||||||
|
log.warning("context(%p): Spurious interrupt (irq: %02x)",
|
||||||
|
(const void *)this,
|
||||||
|
uint(state.irq));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the interrupt is handled the phase indicates entry back to JS,
|
||||||
|
// IRQ is left indicating JS in case we don't trigger the next interrupt.
|
||||||
|
const scope interrupt_return([this, &state]
|
||||||
|
{
|
||||||
|
state.phase = phase::ENTER;
|
||||||
|
state.irq = irq::JS;
|
||||||
|
this->state.store(state, std::memory_order_release);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the user hook if available
|
||||||
|
if(on_intr)
|
||||||
|
{
|
||||||
|
// The user's handler returns -1 for non-overriding behavior
|
||||||
|
const auto ret(on_intr(state.irq));
|
||||||
|
if(ret != -1)
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(state.irq)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case irq::NONE:
|
||||||
|
assert(0);
|
||||||
|
|
||||||
|
case irq::JS:
|
||||||
|
case irq::USER:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case irq::YIELD:
|
||||||
|
ctx::yield();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case irq::TERMINATE:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ircd::js::context::lock::~lock()
|
void
|
||||||
noexcept
|
ircd::js::leave(context &c)
|
||||||
{
|
{
|
||||||
JS_EndRequest(*c);
|
// Load the state to keep the current sem value up to date.
|
||||||
|
// This thread is the only writer to that value.
|
||||||
|
auto state(c.state.load(std::memory_order_relaxed));
|
||||||
|
|
||||||
|
// The ACCEPT phase locks out the interruptor
|
||||||
|
state.phase = phase::ACCEPT;
|
||||||
|
|
||||||
|
// The ACCEPT is released and the current phase seen by interruptor is acquired.
|
||||||
|
state = c.state.exchange(state, std::memory_order_acq_rel);
|
||||||
|
|
||||||
|
// The executor (us) must check if the interruptor (them) has committed to an interrupt
|
||||||
|
// targeting the JS run we are now leaving. JS may have exited after the commitment
|
||||||
|
// and before the interrupt arrival.
|
||||||
|
if(state.phase == phase::INTR)
|
||||||
|
assert(interrupt_poll(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ircd::js::enter(context &c)
|
||||||
|
{
|
||||||
|
// Set the start time which will be observable to the interruptor
|
||||||
|
// after a release sequence.
|
||||||
|
c.started = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// State was already acquired by the last leave();
|
||||||
|
auto state(c.state.load(std::memory_order_relaxed));
|
||||||
|
|
||||||
|
// Increment the semaphore for the next execution;
|
||||||
|
++state.sem;
|
||||||
|
|
||||||
|
// Set the IRQ to JS in case we don't trigger an interrupt, the handler
|
||||||
|
// will see a correct value.
|
||||||
|
state.irq = irq::JS;
|
||||||
|
state.phase = phase::ENTER;
|
||||||
|
|
||||||
|
// Commit to next execution.
|
||||||
|
c.state.store(state, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ircd::js::interrupt(context &c,
|
||||||
|
const interrupt_condition &closure)
|
||||||
|
{
|
||||||
|
// Acquire the execution state.
|
||||||
|
const auto state(c.state.load(std::memory_order_acquire));
|
||||||
|
|
||||||
|
// Only proceed if something is even running.
|
||||||
|
if(state.phase != phase::ENTER)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// See if user still wants an interruption.
|
||||||
|
const auto req(closure());
|
||||||
|
if(req == irq::NONE)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// The expected value of the state to transact.
|
||||||
|
struct context::state in
|
||||||
|
{
|
||||||
|
state.sem, // Expect the sem value to match, else the execution has changed.
|
||||||
|
phase::ENTER, // Expect the execution to be occurring.
|
||||||
|
irq::JS, // JS is always expected here instead of NONE.
|
||||||
|
};
|
||||||
|
|
||||||
|
// The restatement after the transaction.
|
||||||
|
const struct context::state out
|
||||||
|
{
|
||||||
|
state.sem, // Keep the same sem value.
|
||||||
|
phase::INTR, // Indicate our commitment to interrupting.
|
||||||
|
req // Include the irq type.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempt commitment to interrupt here.
|
||||||
|
static const auto order_fail(std::memory_order_relaxed);
|
||||||
|
static const auto order_success(std::memory_order_acq_rel);
|
||||||
|
if(!c.state.compare_exchange_strong(in, out, order_success, order_fail))
|
||||||
|
{
|
||||||
|
// To reliably read from `in` here, order_fail should not be relaxed.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commitment now puts the burden on the executor to not allow this interrupt to bleed into
|
||||||
|
// the next execution, even if JS has already exited before its arrival.
|
||||||
|
interrupt(c.runtime());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ircd::js::interrupt_poll(const context &c)
|
||||||
|
{
|
||||||
|
return JS_CheckForInterrupt(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ircd::js::restore_exception(context &c)
|
||||||
|
{
|
||||||
|
if(unlikely(!c.except))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JS_RestoreExceptionState(c, c.except);
|
||||||
|
c.except = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ircd::js::save_exception(context &c,
|
||||||
|
const JSErrorReport &report)
|
||||||
|
{
|
||||||
|
if(c.except)
|
||||||
|
throw error("(internal error): Won't overwrite saved exception");
|
||||||
|
|
||||||
|
c.except = JS_SaveExceptionState(c),
|
||||||
|
c.report = report;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -1306,7 +1445,7 @@ ircd::js::runtime::runtime(const struct opts &opts)
|
||||||
JS_SetCompartmentNameCallback(get(), handle_compartment_name);
|
JS_SetCompartmentNameCallback(get(), handle_compartment_name);
|
||||||
JS_SetDestroyCompartmentCallback(get(), handle_compartment_destroy);
|
JS_SetDestroyCompartmentCallback(get(), handle_compartment_destroy);
|
||||||
JS_SetContextCallback(get(), handle_context, nullptr);
|
JS_SetContextCallback(get(), handle_context, nullptr);
|
||||||
//JS_SetInterruptCallback(get(), nullptr);
|
JS_SetInterruptCallback(get(), handle_interrupt);
|
||||||
|
|
||||||
JS_SetNativeStackQuota(get(), opts.code_stack_max, opts.trusted_stack_max, opts.untrusted_stack_max);
|
JS_SetNativeStackQuota(get(), opts.code_stack_max, opts.trusted_stack_max, opts.untrusted_stack_max);
|
||||||
}
|
}
|
||||||
|
@ -1353,9 +1492,10 @@ bool
|
||||||
ircd::js::runtime::handle_interrupt(JSContext *const ctx)
|
ircd::js::runtime::handle_interrupt(JSContext *const ctx)
|
||||||
noexcept
|
noexcept
|
||||||
{
|
{
|
||||||
auto &runtime(our(ctx).runtime());
|
log.debug("JSContext(%p): Interrupt", (const void *)ctx);
|
||||||
JS_SetInterruptCallback(runtime, nullptr);
|
|
||||||
return false;
|
auto &c(our(ctx));
|
||||||
|
return c.handle_interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -1440,5 +1580,5 @@ noexcept
|
||||||
msg,
|
msg,
|
||||||
debug(*report).c_str());
|
debug(*report).c_str());
|
||||||
*/
|
*/
|
||||||
push_exception(our(ctx), *report);
|
save_exception(our(ctx), *report);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue