Skip to content

Commit b7f7a55

Browse files
authored
DataLoaders 6: first-class support for NotSupported (#4565)
Introduce a first-class protocol for `DataLoader`s -- whether they are builtin, custom, or external -- to announce that they don't support loading a given piece of data. This fixes one of the oldest issue when loading files in Rerun: loading unsupported data would always bottom out in either the image or websocket paths, leading to unrelated errors very confusing to users. The loading process is now a two-pass process: 1. Dispatch the data to be loaded to all loaders, which will respond ASAP whether they can do it or not. 1.1. If at least one compatible loader is found, go to 2 1.2. Otherwise, fail early with a nice error message for the end user 2. Dispatch the actual data loading. This has important/subtle ramifications on the threading model, but other than that is what you'd expect. Checks: - [x] Native: CLI examples/assets/* - [x] Native: CLI examples/assets - [x] Native: CLI examples/assets/* containing unsupported files - [x] Native: CLI examples/assets containing unsupported files - [x] Native: File>Open examples/assets/* - [x] Native: File>Open examples/assets - [x] Native: File>Open examples/assets/* containing unsupported files - [x] Native: File>Open examples/assets containing unsupported files - [x] Native: Drag-n-drop examples/assets/* - [x] Native: Drag-n-drop examples/assets - [x] Native: Drag-n-drop examples/assets/* containing unsupported files - [x] Native: Drag-n-drop examples/assets containing unsupported files - [x] Web: File>Open examples/assets/* - [x] Web: Drag-n-drop examples/assets/* - [x] Web: File>Open examples/assets/* containing unsupported files - [x] Web: Drag-n-drop examples/assets/* containing unsupported files --- Part of a series of PRs to make it possible to load _any_ file from the local filesystem, by any means, on web and native: - #4516 - #4517 - #4518 - #4519 - #4520 - #4521 - #4565 - #4566 - #4567
1 parent d7b5867 commit b7f7a55

File tree

15 files changed

+256
-101
lines changed

15 files changed

+256
-101
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/re_data_source/src/data_loader/loader_archetype.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ impl DataLoader for ArchetypeLoader {
2626
use anyhow::Context as _;
2727

2828
if filepath.is_dir() {
29-
return Ok(()); // simply not interested
29+
return Err(crate::DataLoaderError::Incompatible(filepath.clone()));
3030
}
3131

3232
re_tracing::profile_function!(filepath.display().to_string());
@@ -45,6 +45,11 @@ impl DataLoader for ArchetypeLoader {
4545
contents: std::borrow::Cow<'_, [u8]>,
4646
tx: std::sync::mpsc::Sender<LoadedData>,
4747
) -> Result<(), crate::DataLoaderError> {
48+
let extension = crate::extension(&filepath);
49+
if !crate::is_supported_file_extension(&extension) {
50+
return Err(crate::DataLoaderError::Incompatible(filepath.clone()));
51+
}
52+
4853
re_tracing::profile_function!(filepath.display().to_string());
4954

5055
let entity_path = EntityPath::from_file_path(&filepath);
@@ -76,8 +81,6 @@ impl DataLoader for ArchetypeLoader {
7681
}
7782
}
7883

79-
let extension = crate::extension(&filepath);
80-
8184
let mut rows = Vec::new();
8285

8386
if crate::SUPPORTED_IMAGE_EXTENSIONS.contains(&extension.as_str()) {

crates/re_data_source/src/data_loader/loader_directory.rs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl crate::DataLoader for DirectoryLoader {
2121
tx: std::sync::mpsc::Sender<crate::LoadedData>,
2222
) -> Result<(), crate::DataLoaderError> {
2323
if dirpath.is_file() {
24-
return Ok(()); // simply not interested
24+
return Err(crate::DataLoaderError::Incompatible(dirpath.clone()));
2525
}
2626

2727
re_tracing::profile_function!(dirpath.display().to_string());
@@ -43,22 +43,28 @@ impl crate::DataLoader for DirectoryLoader {
4343
let filepath = filepath.to_owned();
4444
let tx = tx.clone();
4545

46-
// NOTE: spawn is fine, this whole function is native-only.
47-
rayon::spawn(move || {
48-
let data = match crate::load_file::load(&store_id, &filepath, false, None) {
49-
Ok(data) => data,
50-
Err(err) => {
51-
re_log::error!(?filepath, %err, "Failed to load directory entry");
52-
return;
53-
}
54-
};
46+
// NOTE(1): `spawn` is fine, this whole function is native-only.
47+
// NOTE(2): this must spawned on a dedicated thread to avoid a deadlock!
48+
// `load` will spawn a bunch of loaders on the common rayon thread pool and wait for
49+
// their response via channels: we cannot be waiting for these responses on the
50+
// common rayon thread pool.
51+
_ = std::thread::Builder::new()
52+
.name(format!("load_dir_entry({filepath:?})"))
53+
.spawn(move || {
54+
let data = match crate::load_file::load(&store_id, &filepath, None) {
55+
Ok(data) => data,
56+
Err(err) => {
57+
re_log::error!(?filepath, %err, "Failed to load directory entry");
58+
return;
59+
}
60+
};
5561

56-
for datum in data {
57-
if tx.send(datum).is_err() {
58-
break;
62+
for datum in data {
63+
if tx.send(datum).is_err() {
64+
break;
65+
}
5966
}
60-
}
61-
});
67+
});
6268
}
6369
}
6470

@@ -69,11 +75,11 @@ impl crate::DataLoader for DirectoryLoader {
6975
fn load_from_file_contents(
7076
&self,
7177
_store_id: re_log_types::StoreId,
72-
_path: std::path::PathBuf,
78+
path: std::path::PathBuf,
7379
_contents: std::borrow::Cow<'_, [u8]>,
7480
_tx: std::sync::mpsc::Sender<crate::LoadedData>,
7581
) -> Result<(), crate::DataLoaderError> {
7682
// TODO(cmc): This could make sense to implement for e.g. archive formats (zip, tar, …)
77-
Ok(()) // simply not interested
83+
Err(crate::DataLoaderError::Incompatible(path))
7884
}
7985
}

crates/re_data_source/src/data_loader/loader_external.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ use std::io::Read;
22

33
use once_cell::sync::Lazy;
44

5+
// ---
6+
57
/// To register a new external data loader, simply add an executable in your $PATH whose name
68
/// starts with this prefix.
79
pub const EXTERNAL_DATA_LOADER_PREFIX: &str = "rerun-loader-";
810

11+
/// When an external [`crate::DataLoader`] is asked to load some data that it doesn't know
12+
/// how to load, it should exit with this exit code.
13+
// NOTE: Always keep in sync with other languages.
14+
pub const EXTERNAL_DATA_LOADER_INCOMPATIBLE_EXIT_CODE: i32 = 66;
15+
916
/// Keeps track of the paths all external executable [`crate::DataLoader`]s.
1017
///
1118
/// Lazy initialized the first time a file is opened by running a full scan of the `$PATH`.
@@ -78,14 +85,18 @@ impl crate::DataLoader for ExternalLoader {
7885

7986
re_tracing::profile_function!(filepath.display().to_string());
8087

88+
#[derive(PartialEq, Eq)]
89+
struct CompatibleLoaderFound;
90+
let (tx_feedback, rx_feedback) = std::sync::mpsc::channel::<CompatibleLoaderFound>();
91+
8192
for exe in EXTERNAL_LOADER_PATHS.iter() {
8293
let store_id = store_id.clone();
8394
let filepath = filepath.clone();
8495
let tx = tx.clone();
96+
let tx_feedback = tx_feedback.clone();
8597

86-
// NOTE: spawn is fine, the entire loader is native-only.
8798
rayon::spawn(move || {
88-
re_tracing::profile_function!();
99+
re_tracing::profile_function!(exe.to_string_lossy());
89100

90101
let child = Command::new(exe)
91102
.arg(filepath.clone())
@@ -119,14 +130,28 @@ impl crate::DataLoader for ExternalLoader {
119130
let stdout = std::io::BufReader::new(stdout);
120131
match re_log_encoding::decoder::Decoder::new(version_policy, stdout) {
121132
Ok(decoder) => {
122-
decode_and_stream(&filepath, &tx, decoder);
133+
let filepath = filepath.clone();
134+
let tx = tx.clone();
135+
// NOTE: This is completely IO bound, it must run on a dedicated thread, not the shared
136+
// rayon thread pool.
137+
if let Err(err) = std::thread::Builder::new()
138+
.name(format!("decode_and_stream({filepath:?})"))
139+
.spawn({
140+
let filepath = filepath.clone();
141+
move || {
142+
decode_and_stream(&filepath, &tx, decoder);
143+
}
144+
})
145+
{
146+
re_log::error!(?filepath, loader = ?exe, %err, "Failed to open spawn IO thread");
147+
return;
148+
}
123149
}
124150
Err(re_log_encoding::decoder::DecodeError::Read(_)) => {
125151
// The child was not interested in that file and left without logging
126152
// anything.
127153
// That's fine, we just need to make sure to check its exit status further
128154
// down, still.
129-
return;
130155
}
131156
Err(err) => {
132157
re_log::error!(?filepath, loader = ?exe, %err, "Failed to decode external loader's output");
@@ -142,29 +167,49 @@ impl crate::DataLoader for ExternalLoader {
142167
}
143168
};
144169

145-
if !status.success() {
170+
// NOTE: We assume that plugins are compatible until proven otherwise.
171+
let is_compatible =
172+
status.code() != Some(crate::EXTERNAL_DATA_LOADER_INCOMPATIBLE_EXIT_CODE);
173+
174+
if is_compatible && !status.success() {
146175
let mut stderr = std::io::BufReader::new(stderr);
147176
let mut reason = String::new();
148177
stderr.read_to_string(&mut reason).ok();
149178
re_log::error!(?filepath, loader = ?exe, %reason, "Failed to execute external loader");
150179
}
180+
181+
if is_compatible {
182+
re_log::debug!(loader = ?exe, ?filepath, "compatible external loader found");
183+
tx_feedback.send(CompatibleLoaderFound).ok();
184+
}
151185
});
152186
}
153187

188+
re_tracing::profile_wait!("compatible_loader");
189+
190+
drop(tx_feedback);
191+
192+
let any_compatible_loader = rx_feedback.recv() == Ok(CompatibleLoaderFound);
193+
if !any_compatible_loader {
194+
// NOTE: The only way to get here is if all loaders closed then sending end of the
195+
// channel without sending anything, i.e. none of them are compatible.
196+
return Err(crate::DataLoaderError::Incompatible(filepath.clone()));
197+
}
198+
154199
Ok(())
155200
}
156201

157202
#[inline]
158203
fn load_from_file_contents(
159204
&self,
160205
_store_id: re_log_types::StoreId,
161-
_path: std::path::PathBuf,
206+
path: std::path::PathBuf,
162207
_contents: std::borrow::Cow<'_, [u8]>,
163208
_tx: std::sync::mpsc::Sender<crate::LoadedData>,
164209
) -> Result<(), crate::DataLoaderError> {
165210
// TODO(cmc): You could imagine a world where plugins can be streamed rrd data via their
166211
// standard input… but today is not world.
167-
Ok(()) // simply not interested
212+
Err(crate::DataLoaderError::Incompatible(path))
168213
}
169214
}
170215

crates/re_data_source/src/data_loader/loader_rrd.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl crate::DataLoader for RrdLoader {
2525

2626
let extension = crate::extension(&filepath);
2727
if extension != "rrd" {
28-
return Ok(()); // simply not interested
28+
return Err(crate::DataLoaderError::Incompatible(filepath.clone()));
2929
}
3030

3131
re_log::debug!(
@@ -40,7 +40,17 @@ impl crate::DataLoader for RrdLoader {
4040
let file = std::io::BufReader::new(file);
4141

4242
let decoder = re_log_encoding::decoder::Decoder::new(version_policy, file)?;
43-
decode_and_stream(&filepath, &tx, decoder);
43+
44+
// NOTE: This is IO bound, it must run on a dedicated thread, not the shared rayon thread pool.
45+
std::thread::Builder::new()
46+
.name(format!("decode_and_stream({filepath:?})"))
47+
.spawn({
48+
let filepath = filepath.clone();
49+
move || {
50+
decode_and_stream(&filepath, &tx, decoder);
51+
}
52+
})
53+
.with_context(|| format!("Failed to open spawn IO thread for {filepath:?}"))?;
4454

4555
Ok(())
4656
}
@@ -57,7 +67,7 @@ impl crate::DataLoader for RrdLoader {
5767

5868
let extension = crate::extension(&filepath);
5969
if extension != "rrd" {
60-
return Ok(()); // simply not interested
70+
return Err(crate::DataLoaderError::Incompatible(filepath));
6171
}
6272

6373
let version_policy = re_log_encoding::decoder::VersionPolicy::Warn;
@@ -71,7 +81,9 @@ impl crate::DataLoader for RrdLoader {
7181
_ => return Err(err.into()),
7282
},
7383
};
84+
7485
decode_and_stream(&filepath, &tx, decoder);
86+
7587
Ok(())
7688
}
7789
}

crates/re_data_source/src/data_loader/mod.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ use re_log_types::{ArrowMsg, DataRow, LogMsg};
3131
/// - [`DirectoryLoader`] for recursively loading folders.
3232
/// - [`ExternalLoader`], which looks for user-defined data loaders in $PATH.
3333
///
34+
/// ## Registering custom loaders
35+
///
36+
/// TODO(cmc): web guide in upcoming PR
37+
///
3438
/// ## Execution
3539
///
3640
/// **All** registered [`DataLoader`]s get called when a user tries to open a file, unconditionally.
@@ -131,6 +135,9 @@ pub enum DataLoaderError {
131135
#[error(transparent)]
132136
Decode(#[from] re_log_encoding::decoder::DecodeError),
133137

138+
#[error("No data-loader support for {0:?}")]
139+
Incompatible(std::path::PathBuf),
140+
134141
#[error(transparent)]
135142
Other(#[from] anyhow::Error),
136143
}
@@ -144,6 +151,11 @@ impl DataLoaderError {
144151
_ => false,
145152
}
146153
}
154+
155+
#[inline]
156+
pub fn is_incompatible(&self) -> bool {
157+
matches!(self, Self::Incompatible { .. })
158+
}
147159
}
148160

149161
/// What [`DataLoader`]s load.
@@ -234,9 +246,8 @@ pub use self::loader_archetype::ArchetypeLoader;
234246
pub use self::loader_directory::DirectoryLoader;
235247
pub use self::loader_rrd::RrdLoader;
236248

237-
#[cfg(not(target_arch = "wasm32"))]
238-
pub(crate) use self::loader_external::EXTERNAL_LOADER_PATHS;
239249
#[cfg(not(target_arch = "wasm32"))]
240250
pub use self::loader_external::{
241-
iter_external_loaders, ExternalLoader, EXTERNAL_DATA_LOADER_PREFIX,
251+
iter_external_loaders, ExternalLoader, EXTERNAL_DATA_LOADER_INCOMPATIBLE_EXIT_CODE,
252+
EXTERNAL_DATA_LOADER_PREFIX,
242253
};

crates/re_data_source/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ pub use self::load_file::{extension, load_from_file_contents};
2323
pub use self::web_sockets::connect_to_ws_url;
2424

2525
#[cfg(not(target_arch = "wasm32"))]
26-
pub use self::data_loader::{iter_external_loaders, ExternalLoader};
26+
pub use self::data_loader::{
27+
iter_external_loaders, ExternalLoader, EXTERNAL_DATA_LOADER_INCOMPATIBLE_EXIT_CODE,
28+
EXTERNAL_DATA_LOADER_PREFIX,
29+
};
2730

2831
#[cfg(not(target_arch = "wasm32"))]
2932
pub use self::load_file::load_from_path;
@@ -63,6 +66,7 @@ pub fn supported_extensions() -> impl Iterator<Item = &'static str> {
6366
.iter()
6467
.chain(SUPPORTED_IMAGE_EXTENSIONS)
6568
.chain(SUPPORTED_MESH_EXTENSIONS)
69+
.chain(SUPPORTED_POINT_CLOUD_EXTENSIONS)
6670
.chain(SUPPORTED_TEXT_EXTENSIONS)
6771
.copied()
6872
}

0 commit comments

Comments
 (0)