-
Notifications
You must be signed in to change notification settings - Fork 25
Run comms on the R thread #1075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lionel-
wants to merge
22
commits into
main
Choose a base branch
from
task/sync-comms
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+797
−748
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
c13bcc8
Add `CommHandled` and `ShellHandler` trait extension to Amalthea
lionel- e4aff99
Add blocking comm infrastructure to Ark
lionel- 3b6cf73
Migrate RDataExplorer to the blocking path
lionel- 59020fb
Remove usage of `RThreadSafe` in data explorer comm
lionel- 2814371
Derive Debug as much as possible
lionel- 2a93aad
Rename `get_comm_event_tx` to `comm_event_tx`
lionel- 7e3127b
Remove `r_` prefix now that everything runs on the R thread
lionel- 18e1aac
Rename to `close_on_exit()`
lionel- 037f23f
Improve comments
lionel- ba77c6f
Make events deterministic before Console replies
lionel- 4bfaf4c
Make deterministic assertions in data explorer tests
lionel- ea59ed8
Move comm event channel to comm handler context
lionel- 28c8432
Rename to `ConsoleComm`
lionel- af4bbb2
Add `open_metadata()` method to `CommHandler` trait
lionel- 261fb31
Pass reason to `handle_environment`
lionel- b0e4bb5
Simplify table storage for single thread
lionel- c57caec
Make `CommHandler` non-Send
lionel- 96a73fe
Make `ConsoleComm` fields `pub(crate)`
lionel- e9943f3
Avoid some unwraps
lionel- 039665c
Propagate errors to Amalthea
lionel- cd6ddc4
Remove method in favour of field
lionel- 68692a2
Extract `dispatch_kernel_request()`
lionel- File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ use crate::comm::server_comm::ServerComm; | |||||||||||||||||||||||||||||||||||||||
| use crate::comm::server_comm::ServerStartedMessage; | ||||||||||||||||||||||||||||||||||||||||
| use crate::error::Error; | ||||||||||||||||||||||||||||||||||||||||
| use crate::language::server_handler::ServerHandler; | ||||||||||||||||||||||||||||||||||||||||
| use crate::language::shell_handler::CommHandled; | ||||||||||||||||||||||||||||||||||||||||
| use crate::language::shell_handler::ShellHandler; | ||||||||||||||||||||||||||||||||||||||||
| use crate::socket::comm::CommInitiator; | ||||||||||||||||||||||||||||||||||||||||
| use crate::socket::comm::CommSocket; | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -289,13 +290,13 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| let open_comms = &self.open_comms; | ||||||||||||||||||||||||||||||||||||||||
| let header = req.header.clone(); | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_notification(iopub_tx, req, |msg| { | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_comm_msg(open_comms, header, msg) | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_comm_msg(shell_handler, open_comms, header, msg) | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| Message::CommClose(req) => { | ||||||||||||||||||||||||||||||||||||||||
| let open_comms = &mut self.open_comms; | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_notification(iopub_tx, req, |msg| { | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_comm_close(open_comms, msg) | ||||||||||||||||||||||||||||||||||||||||
| Self::handle_comm_close(shell_handler, open_comms, msg) | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| Message::InspectRequest(req) => Self::handle_request(iopub_tx, socket, req, |msg| { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -452,6 +453,7 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| /// request from the frontend to deliver a message to a backend, often as | ||||||||||||||||||||||||||||||||||||||||
| /// the request side of a request/response pair. | ||||||||||||||||||||||||||||||||||||||||
| fn handle_comm_msg( | ||||||||||||||||||||||||||||||||||||||||
| shell_handler: &mut Box<dyn ShellHandler>, | ||||||||||||||||||||||||||||||||||||||||
| open_comms: &[CommSocket], | ||||||||||||||||||||||||||||||||||||||||
| header: JupyterHeader, | ||||||||||||||||||||||||||||||||||||||||
| msg: &CommWireMsg, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -474,7 +476,6 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| CommMsg::Data(msg.data.clone()) | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Send the message to the comm | ||||||||||||||||||||||||||||||||||||||||
| let Some(comm) = open_comms.iter().find(|c| c.comm_id == msg.comm_id) else { | ||||||||||||||||||||||||||||||||||||||||
| log::warn!( | ||||||||||||||||||||||||||||||||||||||||
| "Received message for unknown comm channel {}: {comm_msg:?}", | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -483,6 +484,13 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Try to dispatch the message to the new handler API | ||||||||||||||||||||||||||||||||||||||||
| match shell_handler.handle_comm_msg(&msg.comm_id, &comm.comm_name, comm_msg.clone())? { | ||||||||||||||||||||||||||||||||||||||||
| CommHandled::Handled => return Ok(()), | ||||||||||||||||||||||||||||||||||||||||
| CommHandled::NotHandled => {}, | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Fall back to old approach for compatibility while we migrate comms | ||||||||||||||||||||||||||||||||||||||||
| log::trace!("Sending message to comm '{}'", comm.comm_name); | ||||||||||||||||||||||||||||||||||||||||
| comm.incoming_tx.send(comm_msg).log_err(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+487
to
496
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
idk, seems like a clearer use of match? |
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -667,7 +675,11 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Handle a request to close a comm | ||||||||||||||||||||||||||||||||||||||||
| fn handle_comm_close(open_comms: &mut Vec<CommSocket>, msg: &CommClose) -> crate::Result<()> { | ||||||||||||||||||||||||||||||||||||||||
| fn handle_comm_close( | ||||||||||||||||||||||||||||||||||||||||
| shell_handler: &mut Box<dyn ShellHandler>, | ||||||||||||||||||||||||||||||||||||||||
| open_comms: &mut Vec<CommSocket>, | ||||||||||||||||||||||||||||||||||||||||
| msg: &CommClose, | ||||||||||||||||||||||||||||||||||||||||
| ) -> crate::Result<()> { | ||||||||||||||||||||||||||||||||||||||||
| let Some(idx) = open_comms.iter().position(|c| c.comm_id == msg.comm_id) else { | ||||||||||||||||||||||||||||||||||||||||
| log::warn!( | ||||||||||||||||||||||||||||||||||||||||
| "Received close message for unknown comm channel {}", | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -676,11 +688,16 @@ impl Shell { | |||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Notify the comm that it's being closed | ||||||||||||||||||||||||||||||||||||||||
| open_comms[idx].incoming_tx.send(CommMsg::Close).log_err(); | ||||||||||||||||||||||||||||||||||||||||
| // Try to dispatch the message to the new handler API. | ||||||||||||||||||||||||||||||||||||||||
| // Fall back to notifying via `incoming_tx` for comms not yet migrated. | ||||||||||||||||||||||||||||||||||||||||
| match shell_handler.handle_comm_close(&msg.comm_id, &open_comms[idx].comm_name)? { | ||||||||||||||||||||||||||||||||||||||||
| CommHandled::Handled => {}, | ||||||||||||||||||||||||||||||||||||||||
| CommHandled::NotHandled => { | ||||||||||||||||||||||||||||||||||||||||
| open_comms[idx].incoming_tx.send(CommMsg::Close).log_err(); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| open_comms.remove(idx); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| log::info!( | ||||||||||||||||||||||||||||||||||||||||
| "Comm channel closed; there are now {} open comms", | ||||||||||||||||||||||||||||||||||||||||
| open_comms.len() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // | ||
| // comm_handler.rs | ||
| // | ||
| // Copyright (C) 2026 Posit Software, PBC. All rights reserved. | ||
| // | ||
| // | ||
|
|
||
| use std::cell::Cell; | ||
| use std::fmt::Debug; | ||
|
|
||
| use amalthea::comm::base_comm::json_rpc_error; | ||
| use amalthea::comm::base_comm::JsonRpcErrorCode; | ||
| use amalthea::comm::comm_channel::CommMsg; | ||
| use amalthea::comm::event::CommEvent; | ||
| use amalthea::socket::comm::CommOutgoingTx; | ||
| use crossbeam::channel::Sender; | ||
| use serde::de::DeserializeOwned; | ||
| use serde::Serialize; | ||
| use stdext::result::ResultExt; | ||
|
|
||
| /// Context provided to `CommHandler` methods, giving access to the outgoing | ||
| /// channel and close-request mechanism. In the future, we'll provide access to | ||
| /// more of the Console state, such as the currently active environment. | ||
| #[derive(Debug)] | ||
| pub struct CommHandlerContext { | ||
| pub outgoing_tx: CommOutgoingTx, | ||
| pub comm_event_tx: Sender<CommEvent>, | ||
| closed: Cell<bool>, | ||
| } | ||
|
|
||
| impl CommHandlerContext { | ||
| pub fn new(outgoing_tx: CommOutgoingTx, comm_event_tx: Sender<CommEvent>) -> Self { | ||
| Self { | ||
| outgoing_tx, | ||
| comm_event_tx, | ||
| closed: Cell::new(false), | ||
| } | ||
| } | ||
|
|
||
| /// Request that Console close this comm after the current handler method | ||
| /// returns. The handler can still send responses or events before and | ||
| /// after calling this since cleanup is deferred. | ||
| pub fn close_on_exit(&self) { | ||
| self.closed.set(true); | ||
| } | ||
|
|
||
| pub fn is_closed(&self) -> bool { | ||
| self.closed.get() | ||
| } | ||
| } | ||
|
|
||
| /// Trait for comm handlers that run synchronously on the R thread. | ||
| /// | ||
| /// All methods are called from the R thread within `ReadConsole`, so R code | ||
| /// can be safely called from these handlers. | ||
| pub trait CommHandler: Debug { | ||
| /// Metadata sent to the frontend in the `comm_open` message | ||
| /// (backend-initiated comms). Default is empty object. | ||
| fn open_metadata(&self) -> serde_json::Value { | ||
| serde_json::json!({}) | ||
| } | ||
|
|
||
| /// Initialise handler state on the R thread (initial scan, first event, | ||
| /// etc.). Default is no-op. | ||
| fn handle_open(&mut self, _ctx: &CommHandlerContext) {} | ||
|
|
||
| /// Handle an incoming message (RPC or data). | ||
| fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext); | ||
|
|
||
| /// Handle comm close. Default is no-op. | ||
| fn handle_close(&mut self, _ctx: &CommHandlerContext) {} | ||
|
|
||
| /// Called when the environment changes. The `event` indicates what | ||
| /// triggered the change so handlers can decide whether to react. | ||
| /// Default is no-op. | ||
| fn handle_environment(&mut self, _event: EnvironmentChanged, _ctx: &CommHandlerContext) {} | ||
| } | ||
|
|
||
| /// Why the environment changed. | ||
| #[derive(Debug, Clone, Copy)] | ||
| pub enum EnvironmentChanged { | ||
| /// A top-level execution completed (user code, debug eval, etc.). | ||
| Execution, | ||
| /// The user selected a different frame in the call stack during debugging. | ||
| FrameSelected, | ||
| } | ||
|
Comment on lines
+81
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i like this framing! |
||
|
|
||
| /// A registered comm in the Console's comm table. | ||
| pub(crate) struct ConsoleComm { | ||
| pub(crate) handler: Box<dyn CommHandler>, | ||
| pub(crate) ctx: CommHandlerContext, | ||
| } | ||
|
|
||
| /// Handle an RPC request from a `CommMsg`. | ||
| /// | ||
| /// Non-RPC messages are logged and ignored. Requests that could not be | ||
| /// handled cause an RPC error response. | ||
| pub fn handle_rpc_request<Reqs, Reps>( | ||
| outgoing_tx: &CommOutgoingTx, | ||
| comm_name: &str, | ||
| message: CommMsg, | ||
| request_handler: impl FnOnce(Reqs) -> anyhow::Result<Reps>, | ||
| ) where | ||
| Reqs: DeserializeOwned + Debug, | ||
| Reps: Serialize, | ||
| { | ||
| let (id, parent_header, data) = match message { | ||
| CommMsg::Rpc { | ||
| id, | ||
| parent_header, | ||
| data, | ||
| } => (id, parent_header, data), | ||
| other => { | ||
| log::warn!("Expected RPC message for {comm_name}, got {other:?}"); | ||
| return; | ||
| }, | ||
| }; | ||
|
|
||
| let json = match serde_json::from_value::<Reqs>(data.clone()) { | ||
| Ok(m) => { | ||
| let _span = | ||
| tracing::trace_span!("comm handler", name = comm_name, request = ?m).entered(); | ||
| match request_handler(m) { | ||
| Ok(reply) => match serde_json::to_value(reply) { | ||
| Ok(value) => value, | ||
| Err(err) => { | ||
| let message = format!( | ||
| "Failed to serialise reply for {comm_name} request: {err} (request: {data})" | ||
| ); | ||
| log::warn!("{message}"); | ||
| json_rpc_error(JsonRpcErrorCode::InternalError, message) | ||
| }, | ||
| }, | ||
| Err(err) => { | ||
| let message = | ||
| format!("Failed to process {comm_name} request: {err} (request: {data})"); | ||
| log::warn!("{message}"); | ||
| json_rpc_error(JsonRpcErrorCode::InternalError, message) | ||
| }, | ||
| } | ||
| }, | ||
| Err(err) => { | ||
| let message = format!( | ||
| "No handler for {comm_name} request (method not found): {err} (request: {data})" | ||
| ); | ||
| log::warn!("{message}"); | ||
| json_rpc_error(JsonRpcErrorCode::MethodNotFound, message) | ||
| }, | ||
| }; | ||
|
|
||
| let response = CommMsg::Rpc { | ||
| id, | ||
| parent_header, | ||
| data: json, | ||
| }; | ||
| outgoing_tx.send(response).log_err(); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you really want to be talking about "kernel main thread" in amalthea docs?