Merge pull request #451 from erg-lang/fix-els

Enhance ELS stability
This commit is contained in:
Shunsuke Shibayama 2023-08-23 18:20:13 +09:00 committed by GitHub
commit 01fae890a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 67 deletions

View file

@ -16,23 +16,35 @@ use lsp_types::{
use crate::server::Server;
#[derive(Debug, Clone)]
pub enum WorkerMessage<P> {
Request(i64, P),
Kill,
}
impl<P> From<(i64, P)> for WorkerMessage<P> {
fn from((id, params): (i64, P)) -> Self {
Self::Request(id, params)
}
}
#[derive(Debug, Clone)]
pub struct SendChannels {
completion: mpsc::Sender<(i64, CompletionParams)>,
resolve_completion: mpsc::Sender<(i64, CompletionItem)>,
goto_definition: mpsc::Sender<(i64, GotoDefinitionParams)>,
semantic_tokens_full: mpsc::Sender<(i64, SemanticTokensParams)>,
inlay_hint: mpsc::Sender<(i64, InlayHintParams)>,
inlay_hint_resolve: mpsc::Sender<(i64, InlayHint)>,
hover: mpsc::Sender<(i64, HoverParams)>,
references: mpsc::Sender<(i64, ReferenceParams)>,
code_lens: mpsc::Sender<(i64, CodeLensParams)>,
code_action: mpsc::Sender<(i64, CodeActionParams)>,
code_action_resolve: mpsc::Sender<(i64, CodeAction)>,
signature_help: mpsc::Sender<(i64, SignatureHelpParams)>,
will_rename_files: mpsc::Sender<(i64, RenameFilesParams)>,
execute_command: mpsc::Sender<(i64, ExecuteCommandParams)>,
pub(crate) health_check: mpsc::Sender<()>,
completion: mpsc::Sender<WorkerMessage<CompletionParams>>,
resolve_completion: mpsc::Sender<WorkerMessage<CompletionItem>>,
goto_definition: mpsc::Sender<WorkerMessage<GotoDefinitionParams>>,
semantic_tokens_full: mpsc::Sender<WorkerMessage<SemanticTokensParams>>,
inlay_hint: mpsc::Sender<WorkerMessage<InlayHintParams>>,
inlay_hint_resolve: mpsc::Sender<WorkerMessage<InlayHint>>,
hover: mpsc::Sender<WorkerMessage<HoverParams>>,
references: mpsc::Sender<WorkerMessage<ReferenceParams>>,
code_lens: mpsc::Sender<WorkerMessage<CodeLensParams>>,
code_action: mpsc::Sender<WorkerMessage<CodeActionParams>>,
code_action_resolve: mpsc::Sender<WorkerMessage<CodeAction>>,
signature_help: mpsc::Sender<WorkerMessage<SignatureHelpParams>>,
will_rename_files: mpsc::Sender<WorkerMessage<RenameFilesParams>>,
execute_command: mpsc::Sender<WorkerMessage<ExecuteCommandParams>>,
pub(crate) health_check: mpsc::Sender<WorkerMessage<()>>,
}
impl SendChannels {
@ -89,25 +101,43 @@ impl SendChannels {
},
)
}
pub(crate) fn close(&self) {
self.completion.send(WorkerMessage::Kill).unwrap();
self.resolve_completion.send(WorkerMessage::Kill).unwrap();
self.goto_definition.send(WorkerMessage::Kill).unwrap();
self.semantic_tokens_full.send(WorkerMessage::Kill).unwrap();
self.inlay_hint.send(WorkerMessage::Kill).unwrap();
self.inlay_hint_resolve.send(WorkerMessage::Kill).unwrap();
self.hover.send(WorkerMessage::Kill).unwrap();
self.references.send(WorkerMessage::Kill).unwrap();
self.code_lens.send(WorkerMessage::Kill).unwrap();
self.code_action.send(WorkerMessage::Kill).unwrap();
self.code_action_resolve.send(WorkerMessage::Kill).unwrap();
self.signature_help.send(WorkerMessage::Kill).unwrap();
self.will_rename_files.send(WorkerMessage::Kill).unwrap();
self.execute_command.send(WorkerMessage::Kill).unwrap();
self.health_check.send(WorkerMessage::Kill).unwrap();
}
}
#[derive(Debug)]
pub struct ReceiveChannels {
pub(crate) completion: mpsc::Receiver<(i64, CompletionParams)>,
pub(crate) resolve_completion: mpsc::Receiver<(i64, CompletionItem)>,
pub(crate) goto_definition: mpsc::Receiver<(i64, GotoDefinitionParams)>,
pub(crate) semantic_tokens_full: mpsc::Receiver<(i64, SemanticTokensParams)>,
pub(crate) inlay_hint: mpsc::Receiver<(i64, InlayHintParams)>,
pub(crate) inlay_hint_resolve: mpsc::Receiver<(i64, InlayHint)>,
pub(crate) hover: mpsc::Receiver<(i64, HoverParams)>,
pub(crate) references: mpsc::Receiver<(i64, ReferenceParams)>,
pub(crate) code_lens: mpsc::Receiver<(i64, CodeLensParams)>,
pub(crate) code_action: mpsc::Receiver<(i64, CodeActionParams)>,
pub(crate) code_action_resolve: mpsc::Receiver<(i64, CodeAction)>,
pub(crate) signature_help: mpsc::Receiver<(i64, SignatureHelpParams)>,
pub(crate) will_rename_files: mpsc::Receiver<(i64, RenameFilesParams)>,
pub(crate) execute_command: mpsc::Receiver<(i64, ExecuteCommandParams)>,
pub(crate) health_check: mpsc::Receiver<()>,
pub(crate) completion: mpsc::Receiver<WorkerMessage<CompletionParams>>,
pub(crate) resolve_completion: mpsc::Receiver<WorkerMessage<CompletionItem>>,
pub(crate) goto_definition: mpsc::Receiver<WorkerMessage<GotoDefinitionParams>>,
pub(crate) semantic_tokens_full: mpsc::Receiver<WorkerMessage<SemanticTokensParams>>,
pub(crate) inlay_hint: mpsc::Receiver<WorkerMessage<InlayHintParams>>,
pub(crate) inlay_hint_resolve: mpsc::Receiver<WorkerMessage<InlayHint>>,
pub(crate) hover: mpsc::Receiver<WorkerMessage<HoverParams>>,
pub(crate) references: mpsc::Receiver<WorkerMessage<ReferenceParams>>,
pub(crate) code_lens: mpsc::Receiver<WorkerMessage<CodeLensParams>>,
pub(crate) code_action: mpsc::Receiver<WorkerMessage<CodeActionParams>>,
pub(crate) code_action_resolve: mpsc::Receiver<WorkerMessage<CodeAction>>,
pub(crate) signature_help: mpsc::Receiver<WorkerMessage<SignatureHelpParams>>,
pub(crate) will_rename_files: mpsc::Receiver<WorkerMessage<RenameFilesParams>>,
pub(crate) execute_command: mpsc::Receiver<WorkerMessage<ExecuteCommandParams>>,
pub(crate) health_check: mpsc::Receiver<WorkerMessage<()>>,
}
pub trait Sendable<R: lsp_types::request::Request + 'static> {
@ -124,7 +154,7 @@ macro_rules! impl_sendable {
.as_ref()
.unwrap()
.$receiver
.send((id, params))
.send($crate::channels::WorkerMessage::Request(id, params))
.unwrap();
}
}

View file

@ -423,8 +423,8 @@ impl CompletionCache {
self.cache.borrow_mut().insert(namespace, items);
}
pub fn _clear(&self, namespace: &str) {
self.cache.borrow_mut().remove(namespace);
pub fn clear(&self) {
self.cache.borrow_mut().clear();
}
pub fn _append(&self, cache: Dict<String, Vec<CompletionItem>>) {

View file

@ -19,9 +19,12 @@ use lsp_types::{
};
use serde_json::json;
use crate::_log;
use crate::channels::WorkerMessage;
use crate::diff::{ASTDiff, HIRDiff};
use crate::server::{
send, send_log, AnalysisResult, DefaultFeatures, ELSResult, Server, HEALTH_CHECKER_ID,
send, send_log, AnalysisResult, DefaultFeatures, ELSResult, Server, ASK_AUTO_SAVE_ID,
HEALTH_CHECKER_ID,
};
use crate::util::{self, NormalizedUrl};
@ -43,6 +46,12 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
} else {
"exec"
};
if let Some((old, new)) = self.analysis_result.get_ast(&uri).zip(self.get_ast(&uri)) {
if ASTDiff::diff(old, &new).is_nop() {
crate::_log!("no changes: {uri}");
return Ok(());
}
}
let mut checker = self.get_checker(path.clone());
let artifact = match checker.build(code.into(), mode) {
Ok(artifact) => {
@ -198,7 +207,8 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}
let params = PublishDiagnosticsParams::new(uri, diagnostics, None);
if self
.client_capas
.init_params
.capabilities
.text_document
.as_ref()
.map(|doc| doc.publish_diagnostics.is_some())
@ -223,6 +233,25 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
move || {
let mut file_vers = Dict::<NormalizedUrl, i32>::new();
loop {
if _self
.client_answers
.borrow()
.get(&ASK_AUTO_SAVE_ID)
.is_none()
{
_self.ask_auto_save().unwrap();
} else if _self
.client_answers
.borrow()
.get(&ASK_AUTO_SAVE_ID)
.is_some_and(|val| {
val["result"].as_array().and_then(|a| a[0].as_str())
== Some("afterDelay")
})
{
_log!("Auto saving is enabled");
break;
}
for uri in _self.file_cache.entries() {
let Some(latest_ver) = _self.file_cache.get_ver(&uri) else {
continue;
@ -247,8 +276,11 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
/// Send an empty `workspace/configuration` request periodically.
/// If there is no response to the request within a certain period of time, terminate the server.
pub fn start_client_health_checker(&self, receiver: Receiver<()>) {
pub fn start_client_health_checker(&self, receiver: Receiver<WorkerMessage<()>>) {
const INTERVAL: Duration = Duration::from_secs(5);
const TIMEOUT: Duration = Duration::from_secs(10);
// let mut self_ = self.clone();
// FIXME: close this thread when the server is restarted
spawn_new_thread(
move || {
loop {
@ -261,7 +293,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
"params": params,
}))
.unwrap();
sleep(Duration::from_secs(10));
sleep(INTERVAL);
}
},
"start_client_health_checker_sender",
@ -269,14 +301,20 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
spawn_new_thread(
move || {
loop {
match receiver.recv_timeout(Duration::from_secs(20)) {
match receiver.recv_timeout(TIMEOUT) {
Ok(WorkerMessage::Kill) => {
break;
}
Ok(_) => {
// send_log("client health check passed").unwrap();
}
Err(_) => {
// send_log("client health check timed out").unwrap();
lsp_log!("client health check timed out");
std::process::exit(1);
lsp_log!("Client health check timed out");
// lsp_log!("Restart the server");
// _log!("Restart the server");
// send_error_info("Something went wrong, ELS has been restarted").unwrap();
// self_.restart();
panic!("Client health check timed out");
}
}
}

View file

@ -63,6 +63,10 @@ impl ASTDiff {
.unwrap_or(Self::Nop),
}
}
pub const fn is_nop(&self) -> bool {
matches!(self, Self::Nop)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View file

@ -58,6 +58,10 @@ impl FileCache {
}
}
pub fn clear(&self) {
self.files.borrow_mut().clear();
}
fn load_once(&self, uri: &NormalizedUrl) -> ELSResult<()> {
if self.files.borrow_mut().get(uri).is_some() {
return Ok(());

View file

@ -32,19 +32,20 @@ use lsp_types::request::{
WillRenameFiles,
};
use lsp_types::{
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability,
CodeLensOptions, CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
ExecuteCommandOptions, HoverProviderCapability, InitializeResult, InlayHintOptions,
InlayHintServerCapabilities, OneOf, Position, SemanticTokenType, SemanticTokensFullOptions,
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
ServerCapabilities, SignatureHelpOptions, WorkDoneProgressOptions,
CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CodeLensOptions,
CompletionOptions, ConfigurationItem, ConfigurationParams, DidChangeTextDocumentParams,
DidOpenTextDocumentParams, ExecuteCommandOptions, HoverProviderCapability, InitializeParams,
InitializeResult, InlayHintOptions, InlayHintServerCapabilities, OneOf, Position,
SemanticTokenType, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions,
WorkDoneProgressOptions,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value;
use crate::channels::{SendChannels, Sendable};
use crate::channels::{SendChannels, Sendable, WorkerMessage};
use crate::completion::CompletionCache;
use crate::file_cache::FileCache;
use crate::hir_visitor::HIRVisitor;
@ -52,6 +53,7 @@ use crate::message::{ErrorMessage, LSPResult, LogMessage, ShowMessage};
use crate::util::{self, NormalizedUrl};
pub const HEALTH_CHECKER_ID: i64 = 10000;
pub const ASK_AUTO_SAVE_ID: i64 = 10001;
pub type ELSResult<T> = Result<T, Box<dyn std::error::Error>>;
@ -312,7 +314,8 @@ pub struct Server<Checker: BuildRunnable = HIRBuilder, Parser: Parsable = Simple
pub(crate) cfg: ErgConfig,
pub(crate) home: PathBuf,
pub(crate) erg_path: PathBuf,
pub(crate) client_capas: ClientCapabilities,
pub(crate) init_params: InitializeParams,
pub(crate) client_answers: Shared<Dict<i64, Value>>,
pub(crate) disabled_features: Vec<DefaultFeatures>,
pub(crate) opt_features: Vec<OptionalFeatures>,
pub(crate) file_cache: FileCache,
@ -332,7 +335,8 @@ impl<C: BuildRunnable, P: Parsable> Clone for Server<C, P> {
cfg: self.cfg.clone(),
home: self.home.clone(),
erg_path: self.erg_path.clone(),
client_capas: self.client_capas.clone(),
init_params: self.init_params.clone(),
client_answers: self.client_answers.clone(),
disabled_features: self.disabled_features.clone(),
opt_features: self.opt_features.clone(),
file_cache: self.file_cache.clone(),
@ -354,7 +358,8 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
cfg,
home: normalize_path(std::env::current_dir().unwrap_or_default()),
erg_path: erg_path().clone(), // already normalized
client_capas: ClientCapabilities::default(),
init_params: InitializeParams::default(),
client_answers: Shared::new(Dict::new()),
disabled_features: vec![],
opt_features: vec![],
file_cache: FileCache::new(),
@ -390,7 +395,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
send_log("initializing ELS")?;
// #[allow(clippy::collapsible_if)]
if msg.get("params").is_some() && msg["params"].get("capabilities").is_some() {
self.client_capas = ClientCapabilities::deserialize(&msg["params"]["capabilities"])?;
self.init_params = InitializeParams::deserialize(&msg["params"])?;
// send_log(format!("set client capabilities: {:?}", self.client_capas))?;
}
let mut args = self.cfg.runtime_args.iter();
@ -503,7 +508,22 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
capabilities
}
fn init_services(&mut self) {
pub(crate) fn ask_auto_save(&self) -> ELSResult<()> {
let params = ConfigurationParams {
items: vec![ConfigurationItem {
scope_uri: None,
section: Some("files.autoSave".to_string()),
}],
};
send(&json!({
"jsonrpc": "2.0",
"id": ASK_AUTO_SAVE_ID,
"method": "workspace/configuration",
"params": params,
}))
}
fn start_language_services(&mut self) {
let (senders, receivers) = SendChannels::new();
self.channels = Some(senders);
self.start_service::<Completion>(receivers.completion, Self::handle_completion);
@ -544,10 +564,14 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
receivers.execute_command,
Self::handle_execute_command,
);
self.start_auto_diagnostics();
self.start_client_health_checker(receivers.health_check);
}
fn init_services(&mut self) {
self.start_language_services();
self.start_auto_diagnostics();
}
fn exit(&self) -> ELSResult<()> {
send_log("exiting ELS")?;
std::process::exit(0);
@ -562,6 +586,17 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
}))
}
#[allow(unused)]
pub(crate) fn restart(&mut self) {
self.file_cache.clear();
self.comp_cache.clear();
self.modules = ModuleCache::new();
self.analysis_result = AnalysisResultCache::new();
self.current_sig = None;
self.channels.as_ref().unwrap().close();
self.start_language_services();
}
/// Copied and modified from RLS, https://github.com/rust-lang/rls/blob/master/rls/src/server/io.rs
fn read_message(&self) -> Result<Value, io::Error> {
// Read in the "Content-Length: xx" part.
@ -654,7 +689,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
fn start_service<R>(
&self,
receiver: mpsc::Receiver<(i64, R::Params)>,
receiver: mpsc::Receiver<WorkerMessage<R::Params>>,
handler: Handler<Server<Checker, Parser>, R::Params, R::Result>,
) where
R: lsp_types::request::Request + 'static,
@ -664,8 +699,15 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
let mut _self = self.clone();
spawn_new_thread(
move || loop {
let (id, params) = receiver.recv().unwrap();
let _ = send(&LSPResult::new(id, handler(&mut _self, params).unwrap()));
let msg = receiver.recv().unwrap();
match msg {
WorkerMessage::Request(id, params) => {
let _ = send(&LSPResult::new(id, handler(&mut _self, params).unwrap()));
}
WorkerMessage::Kill => {
break;
}
}
},
fn_name!(),
);
@ -742,16 +784,25 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
fn handle_response(&mut self, id: i64, msg: &Value) -> ELSResult<()> {
match id {
HEALTH_CHECKER_ID => {
self.channels.as_ref().unwrap().health_check.send(())?;
self.channels
.as_ref()
.unwrap()
.health_check
.send(WorkerMessage::Request(0, ()))?;
}
_ => {
_log!("received a unknown response: {msg}");
// ignore at this time
_log!("msg: {msg}");
if msg.get("error").is_none() {
self.client_answers.borrow_mut().insert(id, msg.clone());
}
}
}
Ok(())
}
/// TODO: Reuse cache.
/// Because of the difficulty of caching "transitional types" such as assert casting and mutable dependent types,
/// the cache is deleted after each analysis.
pub(crate) fn get_checker(&self, path: PathBuf) -> Checker {
if let Some(shared) = self.get_shared() {
let shared = shared.clone();
@ -892,9 +943,7 @@ impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
if let Some(module) = self.modules.remove(uri) {
let shared = module.context.shared();
let path = util::uri_to_path(uri);
shared.mod_cache.remove(&path);
shared.index.remove_path(&path);
shared.graph.remove(&path);
shared.clear(&path);
}
}
}

View file

@ -50,3 +50,8 @@ where
.spawn(run)
.unwrap()
}
pub fn safe_yield() {
std::thread::yield_now();
std::thread::sleep(std::time::Duration::from_millis(10));
}

View file

@ -254,6 +254,10 @@ pub trait Stream<T>: Sized {
fn split_off(&mut self, at: usize) -> Vec<T> {
self.ref_mut_payload().split_off(at)
}
fn retain(&mut self, f: impl FnMut(&T) -> bool) {
self.ref_mut_payload().retain(f);
}
}
#[macro_export]

View file

@ -54,27 +54,37 @@ impl SharedCompilerResource {
_self
}
/// Clear all but builtin modules
pub fn clear_all(&self) {
self.mod_cache.initialize();
self.py_mod_cache.initialize();
self.index.initialize();
self.graph.initialize();
self.trait_impls.initialize();
// self.trait_impls.initialize();
self.promises.initialize();
self.errors.clear();
self.warns.clear();
}
/// Clear all information about the module. All child modules are also cleared.
pub fn clear(&self, path: &Path) {
for child in self.graph.children(path) {
self.clear(&child);
}
self.mod_cache.remove(path);
self.py_mod_cache.remove(path);
self.index.remove_path(path);
self.graph.remove(path);
self.promises.remove(path);
// self.errors.remove(path);
// self.warns.remove(path);
}
pub fn rename_path(&self, old: &Path, new: PathBuf) {
self.mod_cache.rename_path(old, new.clone());
self.py_mod_cache.rename_path(old, new.clone());
self.index.rename_path(old, new.clone());
self.graph.rename_path(old, new);
self.graph.rename_path(old, new.clone());
self.promises.rename(old, new);
}
}

View file

@ -137,6 +137,7 @@ impl ModuleGraph {
Ok(())
}
/// Do not erase relationships with modules that depend on `path`
pub fn remove(&mut self, path: &Path) {
let path = NormalizedPathBuf::new(path.to_path_buf());
self.0.retain(|n| n.id != path);

View file

@ -61,6 +61,15 @@ impl TraitImpls {
self.cache.remove(path)
}
pub fn rename<Q: Eq + Hash + ?Sized>(&mut self, old: &Q, new: Str)
where
Str: Borrow<Q>,
{
if let Some(impls) = self.remove(old) {
self.register(new, impls);
}
}
pub fn initialize(&mut self) {
self.cache.clear();
}
@ -123,6 +132,13 @@ impl SharedTraitImpls {
self.0.borrow_mut().remove(path)
}
pub fn rename<Q: Eq + Hash + ?Sized>(&self, old: &Q, new: Str)
where
Str: Borrow<Q>,
{
self.0.borrow_mut().rename(old, new);
}
pub fn ref_inner(&self) -> MappedRwLockReadGuard<Dict<Str, Set<TraitImpl>>> {
RwLockReadGuard::map(self.0.borrow(), |tis| &tis.cache)
}

View file

@ -5,6 +5,7 @@ use std::thread::{current, JoinHandle, ThreadId};
use erg_common::dict::Dict;
use erg_common::pathutil::NormalizedPathBuf;
use erg_common::shared::Shared;
use erg_common::spawn::safe_yield;
use super::SharedModuleGraph;
@ -102,6 +103,21 @@ impl SharedPromises {
.insert(path, Promise::running(handle));
}
pub fn remove(&self, path: &Path) {
self.promises.borrow_mut().remove(path);
}
pub fn initialize(&self) {
self.promises.borrow_mut().clear();
}
pub fn rename(&self, old: &Path, new: PathBuf) {
let Some(promise) = self.promises.borrow_mut().remove(old) else {
return;
};
self.promises.borrow_mut().insert(new.into(), promise);
}
pub fn is_registered(&self, path: &Path) -> bool {
self.promises.borrow().get(path).is_some()
}
@ -140,7 +156,7 @@ impl SharedPromises {
.get(path)
.is_some_and(|p| !p.is_finished())
{
std::thread::yield_now();
safe_yield();
}
return Ok(());
}
@ -152,7 +168,7 @@ impl SharedPromises {
pub fn join(&self, path: &Path) -> std::thread::Result<()> {
while let Some(Promise::Joining) | None = self.promises.borrow().get(path) {
std::thread::yield_now();
safe_yield();
}
let promise = self.promises.borrow_mut().get_mut(path).unwrap().take();
self.join_checked(path, promise)