blob: f08ea025ac5fe173afacfbbdbd99d8bbc0b73437 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
use super::{
Captured, Command, Health, Limits, Module, PollClock, Result, State, Statistics,
captured::Target, engine, failed,
};
use deno_core::v8;
use futures_util::{StreamExt, future::LocalBoxFuture, stream::FuturesUnordered};
use std::{
collections::BTreeMap,
future::poll_fn,
sync::{Arc, atomic::Ordering},
task::Poll,
};
use tokio::sync::{OwnedSemaphorePermit, mpsc, oneshot, watch};
struct Loaded {
code: engine::Module,
calls: usize,
closing: bool,
callbacks: BTreeMap<u64, engine::Callbacks>,
}
struct Request {
module: Module,
reply: oneshot::Sender<Result<serde_json::Value>>,
_bytes: OwnedSemaphorePermit,
context: Option<Arc<super::request::Owner>>,
}
type Pending = FuturesUnordered<LocalBoxFuture<'static, (u64, Result<v8::Global<v8::Value>>)>>;
pub(super) async fn run(
mut commands: mpsc::UnboundedReceiver<Command>,
health: Arc<Health>,
limits: Limits,
realm: super::Realm,
) -> Result<()> {
let mut states: BTreeMap<u64, watch::Sender<State>> = BTreeMap::new();
let mut requests = BTreeMap::<u64, Request>::new();
let result = async {
// Trusted runtime setup precedes the user-script clock. Keep its errors
// in this result so queued loads still receive the common cleanup below.
let mut runtime = match realm {
super::Realm::Pure => engine::pure_runtime(&health, &limits)?,
super::Realm::Scoped => engine::scoped_runtime(&health, &limits)?,
};
health.initializing.store(false, Ordering::SeqCst);
let serving = async {
let mut modules = BTreeMap::<u64, Loaded>::new();
let mut pending = Pending::new();
let mut next_call = 0_u64;
let mut next_callback = 0_u64;
loop {
enum Ready {
Command(Option<Command>),
Complete(u64, Result<v8::Global<v8::Value>>),
}
let ready = tokio::select! {
biased;
_ = health.stopped.cancelled() => return Err(health.error()),
value = poll_fn(|cx| {
if let Poll::Ready(Some((id, result))) = pending.poll_next_unpin(cx) { return Poll::Ready(Ok(Ready::Complete(id, result))); }
if let Poll::Ready(Err(error)) = runtime.poll_event_loop(cx, Default::default()) { return Poll::Ready(Err(failed(error))); }
if let Poll::Ready(Some((id, result))) = pending.poll_next_unpin(cx) { return Poll::Ready(Ok(Ready::Complete(id, result))); }
commands.poll_recv(cx).map(|command| Ok(Ready::Command(command)))
}) => value?,
};
match ready {
Ready::Command(None) => return Ok(()),
Ready::Command(Some(command)) => match command {
Command::Load {
id,
name,
source,
state,
bytes,
} => {
match engine::load(&mut runtime, &name, &source) {
Ok(code) => {
modules.insert(
id,
Loaded {
code,
calls: 0,
closing: false,
callbacks: BTreeMap::new(),
},
);
state.send_replace(State::Ready);
states.insert(id, state);
}
Err(error) => {
state.send_replace(State::Closed(Some(error)));
}
}
drop(bytes);
}
Command::Call {
module,
target,
args,
context,
reply,
bytes,
} => {
let Some(loaded) =
modules.get_mut(&module.0.id).filter(|loaded| !loaded.closing)
else {
drop(context);
let _ = reply.send(Err(failed("module is retired")));
continue;
};
let call = match target {
Target::Path(path) => {
engine::call(&mut runtime, &loaded.code, &path, &args)
}
Target::Captured { callback, method } => loaded
.callbacks
.get(&callback)
.ok_or_else(|| failed("callback registration released"))
.and_then(|callbacks| {
engine::call_bound(&mut runtime, callbacks, &method, &args, context.as_ref())
}),
};
match call {
Ok(call) => {
next_call = next_call
.checked_add(1)
.ok_or_else(|| failed("call identity exhausted"))?;
let id = next_call;
loaded.calls += 1;
requests.insert(
id,
Request {
module,
reply,
_bytes: bytes,
context,
},
);
pending.push(Box::pin(async move { (id, call.await) }));
}
Err(error) => {
drop(context);
let _ = reply.send(Err(error));
}
}
}
Command::Capture {
module, namespace, methods, reply, bytes: _bytes,
} => {
if reply.is_closed() {
continue;
}
let Some(loaded) = modules
.get_mut(&module.0.id)
.filter(|m| !m.closing)
else {
let _ = reply.send(Err(failed("module is retired")));
continue;
};
let names = methods.iter().map(String::as_str).collect::<Vec<_>>();
match engine::capture_callbacks(
&mut runtime, &loaded.code, &namespace, &names,
) {
Ok(callbacks) => {
next_callback = next_callback
.checked_add(1)
.ok_or_else(|| failed("callback identity exhausted"))?;
let id = next_callback;
loaded.callbacks.insert(id, callbacks);
// The payload owns its release obligation even if send succeeds
// but the awaiting future is dropped before receiving it.
let _ = reply.send(Ok(Captured::new(module, id, methods)));
}
Err(error) => {
let _ = reply.send(Err(error));
}
}
}
Command::ReleaseCaptured { module, callback } => {
if let Some(loaded) = modules.get_mut(&module) {
loaded.callbacks.remove(&callback);
}
}
Command::Close(id) => {
if let Some(module) = modules.get_mut(&id) {
module.closing = true;
if module.calls == 0 {
close(&mut modules, &mut states, id);
}
}
}
Command::Statistics {
collect,
reply,
} => {
if collect {
runtime.v8_isolate().low_memory_notification();
}
let _ = reply.send(Statistics {
modules: modules.len(),
pending_calls: requests.len(),
captured_callbacks: modules.values().map(|m| m.callbacks.len()).sum(),
used_heap_bytes: runtime
.v8_isolate()
.get_heap_statistics()
.used_heap_size(),
});
}
},
Ready::Complete(id, result) => {
let mut request = requests.remove(&id).expect("pending call has an owner");
let result = result.and_then(|value| engine::value(&mut runtime, value));
// Context capabilities expire before completion becomes observable.
drop(request.context.take());
let _ = request.reply.send(result);
if let Some(module) = modules.get_mut(&request.module.0.id) {
module.calls -= 1;
if module.closing && module.calls == 0 {
close(&mut modules, &mut states, request.module.0.id);
}
}
}
}
// Keep command floods from creating one unlimited synchronous poll.
tokio::task::yield_now().await;
}
};
let mut serving = std::pin::pin!(serving);
poll_fn(|cx| {
health.polling.send_replace(Some(PollClock {
started: tokio::time::Instant::now(),
initializing: health.initializing.load(Ordering::SeqCst),
}));
let result = serving.as_mut().poll(cx);
health.polling.send_replace(None);
result
})
.await
}
.await;
// Runtime, module globals and pending promises have left scope before cleanup
// acknowledgements. Host resource leases remain with the original application owner.
if let Err(error) = &result {
health.fail(error.clone());
}
let failure = result.as_ref().err().cloned();
// Revoke accepted and queued contexts before any close acknowledgement.
for request in requests.values_mut() {
drop(request.context.take());
}
commands.close();
let mut queued_replies = Vec::new();
let mut queued_states = Vec::new();
while let Some(command) = commands.recv().await {
match command {
Command::Call { context, reply, .. } => {
drop(context);
queued_replies.push(reply);
}
Command::Load { state, .. } => queued_states.push(state),
_ => {}
}
}
for (_, state) in states {
state.send_replace(State::Closed(failure.clone()));
}
for state in queued_states {
state.send_replace(State::Closed(Some(health.error())));
}
for (_, request) in requests {
let _ = request.reply.send(Err(health.error()));
}
for reply in queued_replies {
let _ = reply.send(Err(health.error()));
}
result
}
fn close(
modules: &mut BTreeMap<u64, Loaded>,
states: &mut BTreeMap<u64, watch::Sender<State>>,
id: u64,
) {
modules.remove(&id);
if let Some(state) = states.remove(&id) {
state.send_replace(State::Closed(None));
}
}