0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-06-02 18:18:56 +02:00

ircd::js: Add wait-free execution interruption.

This commit is contained in:
Jason Volk 2016-10-20 03:25:41 -07:00
parent cf71fd5c5e
commit ad1fef4247
3 changed files with 366 additions and 157 deletions

View file

@ -25,34 +25,66 @@
namespace ircd {
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
:private custom_ptr<JSContext>
{
class lock
{
context *c;
using steady_clock = std::chrono::steady_clock;
using time_point = steady_clock::time_point;
public:
lock(context &c); // BeginRequest on cx of your choice
lock(); // BeginRequest on the thread_local cx
~lock() noexcept; // EndRequest
struct alignas(8) state
{
uint32_t sem;
enum phase phase;
enum irq irq;
};
struct opts
{
size_t stack_chunk_size = 8_KiB;
bool dtor_gc = true;
size_t stack_chunk_size = 8_KiB;
microseconds timer_interval = 250000us;
microseconds timer_limit = 500000us;
};
struct exstate
{
JSExceptionState *state; // exstate
JSErrorReport report; // note: ptrs within are not necessarily safe
};
struct opts opts;
struct opts opts; // We keep a copy of the given opts here.
std::stack<struct exstate> exstate;
// Interruption state
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 custom_ptr<JSContext>::operator*(); }
bool operator!() const { return !custom_ptr<JSContext>::operator bool(); }
@ -61,12 +93,14 @@ struct context
auto ptr() const { 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() = default;
context(context &&) noexcept;
context(context &&) = delete;
context(const context &) = delete;
context &operator=(context &&) noexcept;
context &operator=(const context &) = delete;
~context() noexcept;
};
@ -75,10 +109,6 @@ struct context
// Do not construct more than one context on the same thread- this is overwritten.
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
const context &our(const 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 &);
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 uncaught_exception(const context &c) { return JS_IsExceptionPending(c); }
inline auto interrupted(const context &c) { return JS_CheckForInterrupt(c); }
inline auto version(const context &c) { return version(JS_GetVersion(c)); }
JSObject *current_global(context &c);
JSObject *current_global(); // thread_local
// Memory
inline void out_of_memory(context &c) { JS_ReportOutOfMemory(c); }
inline void allocation_overflow(context &c) { JS_ReportAllocationOverflow(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
JSObject *current_global();
// Exception
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 *
current_global()
@ -110,12 +186,6 @@ current_global()
return current_global(*cx);
}
inline bool
rethrow_exception(context &c)
{
return JS_ReportPendingException(c);
}
inline JSObject *
current_global(context &c)
{
@ -126,7 +196,8 @@ inline void
priv(context &c,
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);
}
@ -158,3 +229,5 @@ our(const JSContext *const &c)
} // namespace js
} // namespace ircd
static_assert(sizeof(struct ircd::js::context::state) == 8, "");

View file

@ -71,10 +71,6 @@ class runtime
// Do not construct more than one runtime on the same thread- this is overwritten.
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
const runtime &our(const JSRuntime *const &);
runtime &our(JSRuntime *const &);
@ -83,6 +79,7 @@ runtime &our(JSRuntime *const &);
const runtime &object_runtime(const JSObject &);
runtime &object_runtime(JSObject &);
// Do not call interrupt() unless you know what you're doing; see context.h.
void interrupt(runtime &r);
void run_gc(runtime &r);
@ -96,7 +93,6 @@ run_gc(runtime &r)
inline void
interrupt(runtime &r)
{
JS_SetInterruptCallback(r, runtime::handle_interrupt);
JS_RequestInterruptCallback(r);
}

View file

@ -32,13 +32,9 @@ namespace js {
__thread runtime *rt;
__thread context *cx;
// Location of the main JSRuntime instance. An extern reference to this exists in js/runtime.h.
// It is null until js::init manually constructs (and later destructs) it.
runtime main;
// Location of the default/main JSContext instance. An extern reference to this exists in js/context.h.
// Lifetime similar to main runtime
context mc;
// Location of the main JSRuntime and JSContext instances.
runtime *main_runtime;
context *main_context;
// Logging facility for this submodule with SNOMASK.
struct log::log log
@ -55,12 +51,18 @@ struct log::log log
//
ircd::js::init::init()
try
{
log.info("Initializing the JS engine [%s: %s]",
"SpiderMonkey",
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())
throw error("JS_Init(): failure");
@ -69,38 +71,35 @@ try
log.info("Initializing the main JS Runtime (main_maxbytes: %zu)",
runtime_opts.maxbytes);
main = runtime(runtime_opts);
mc = context(main, context_opts);
main_runtime = new runtime(runtime_opts);
main_context = new context(*main_runtime, context_opts);
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");
}
}
catch(...)
{
this->~init();
throw;
}
ircd::js::init::~init()
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");
}
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
mc = context{};
main = runtime{};
log.info("Terminating the JS engine");
log.info("Terminating the JS Engine");
JS_ShutDown();
}
@ -855,7 +854,10 @@ ircd::js::jserror::jserror(pending_t)
:ircd::js::error{generate_skip}
,val{*cx}
{
auto report(pop_exception(*cx));
if(!restore_exception(*cx))
return;
auto &report(cx->report);
if(report.flags & JSREPORT_EXCEPTION &&
report.errorNumber != 105)
{
@ -1161,8 +1163,23 @@ ircd::js::context::context(JSRuntime *const &runtime,
const struct opts &opts)
:custom_ptr<JSContext>
{
JS_NewContext(runtime, opts.stack_chunk_size),
[opts](JSContext *const ctx) //TODO: old gcc/clang can't copy from opts.dtor_gc
// Construct the context
[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
{
if(!ctx)
@ -1171,111 +1188,233 @@ ircd::js::context::context(JSRuntime *const &runtime,
// Free the user's privdata managed object
delete static_cast<const privdata *>(JS_GetSecondContextPrivate(ctx));
if(opts.dtor_gc)
JS_DestroyContext(ctx);
else
JS_DestroyContextNoGC(ctx);
JS_DestroyContext(ctx);
// Indicate to thread_local
cx = nullptr;
}
}
,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
// 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()
noexcept
{
// Branch not taken on std::move()
if(!!*this)
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;
interval.store(microseconds(0), std::memory_order_release);
timer.join();
}
void
ircd::js::push_exception(context &c,
const JSErrorReport &report)
ircd::js::context::timer_worker()
{
c.exstate.push
({
JS_SaveExceptionState(c),
report
});
if(unlikely(!c.exstate.top().state))
while(interval.load(std::memory_order_consume) > microseconds(0))
{
c.exstate.pop();
throw error("(internal error) No pending exception to save");
std::this_thread::sleep_for(interval.load(std::memory_order_consume));
timer_check();
}
}
ircd::js::context::lock::lock()
:lock{*cx}
void
ircd::js::context::timer_check()
{
// 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)
:c{&c}
bool
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()
noexcept
void
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_SetDestroyCompartmentCallback(get(), handle_compartment_destroy);
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);
}
@ -1353,9 +1492,10 @@ bool
ircd::js::runtime::handle_interrupt(JSContext *const ctx)
noexcept
{
auto &runtime(our(ctx).runtime());
JS_SetInterruptCallback(runtime, nullptr);
return false;
log.debug("JSContext(%p): Interrupt", (const void *)ctx);
auto &c(our(ctx));
return c.handle_interrupt();
}
bool
@ -1440,5 +1580,5 @@ noexcept
msg,
debug(*report).c_str());
*/
push_exception(our(ctx), *report);
save_exception(our(ctx), *report);
}