Skip to content
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

fix: Avoid serializing minidumps over procspawn #508

Merged
merged 6 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
### Internal

- Measure how long it takes to download sources from external servers. ([#483](https://github.com/getsentry/symbolicator/pull/483))
- Avoid serialization overhead when processing large minidumps out-of-process. ([#508](https://github.com/getsentry/symbolicator/pull/508))

## 0.3.4

Expand Down
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/symbolicator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ anyhow = "1.0.38"
apple-crash-report-parser = "0.4.2"
backtrace = "0.3.55"
base64 = "0.13.0"
bytes = { version = "1.0.1", features = ["serde"] }
cadence = "0.25.0"
chrono = { version = "0.4.19", features = ["serde"] }
console = "0.14.0"
Expand Down
6 changes: 5 additions & 1 deletion crates/symbolicator/src/endpoints/applecrashreport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ async fn handle_apple_crash_report_request(

let content_disposition = field.content_disposition();
match content_disposition.as_ref().and_then(|d| d.get_name()) {
Some("apple_crash_report") => report = Some(read_multipart_file(field).await?),
Some("apple_crash_report") => {
let mut report_file = tempfile::tempfile()?;
read_multipart_file(field, &mut report_file).await?;
report = Some(report_file)
}
Some("sources") => sources = read_multipart_sources(field).await?.into(),
Some("options") => options = read_multipart_request_options(field).await?,
_ => (), // Always ignore unknown fields.
Expand Down
16 changes: 13 additions & 3 deletions crates/symbolicator/src/endpoints/minidump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,27 @@ async fn handle_minidump_request(

let content_disposition = field.content_disposition();
match content_disposition.as_ref().and_then(|d| d.get_name()) {
Some("upload_file_minidump") => minidump = Some(read_multipart_file(field).await?),
Some("upload_file_minidump") => {
let minidump_file = tempfile::Builder::new()
.prefix("minidump")
.suffix(".dmp")
.tempfile()?;
let (mut file, temp_path) = minidump_file.into_parts();

read_multipart_file(field, &mut file).await?;

minidump = Some(temp_path)
}
Some("sources") => sources = read_multipart_sources(field).await?.into(),
Some("options") => options = read_multipart_request_options(field).await?,
_ => (), // Always ignore unknown fields.
}
}

let minidump = minidump.ok_or_else(|| error::ErrorBadRequest("missing minidump"))?;
let minidump_file = minidump.ok_or_else(|| error::ErrorBadRequest("missing minidump"))?;

let symbolication = state.symbolication();
let request_id = symbolication.process_minidump(params.scope, minidump, sources, options);
let request_id = symbolication.process_minidump(params.scope, minidump_file, sources, options);

match symbolication.get_response(request_id, params.timeout).await {
Some(response) => Ok(Json(response)),
Expand Down
140 changes: 69 additions & 71 deletions crates/symbolicator/src/services/symbolication.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use std::collections::{BTreeMap, BTreeSet};
use std::convert::TryInto;
use std::fs::File;
use std::future::Future;
use std::io::{Cursor, Write};
use std::iter::FromIterator;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};

use anyhow::Context;
use apple_crash_report_parser::AppleCrashReport;
use bytes::Bytes;
use chrono::{DateTime, TimeZone, Utc};
use futures::{channel::oneshot, future, FutureExt as _};
use parking_lot::Mutex;
Expand All @@ -27,6 +26,7 @@ use symbolic::minidump::cfi::CfiCache;
use symbolic::minidump::processor::{
CodeModule, CodeModuleId, FrameTrust, ProcessMinidumpError, ProcessState, RegVal,
};
use tempfile::TempPath;
use thiserror::Error;

use crate::cache::CacheStatus;
Expand Down Expand Up @@ -1692,8 +1692,6 @@ impl SymbolicationActor {
handle: procspawn::JoinHandle<Result<procspawn::serde::Json<T>, E>>,
timeout: Duration,
metric: &str,
minidump: &[u8],
minidump_cache: crate::cache::Cache,
) -> Result<T, anyhow::Error>
where
T: Serialize + DeserializeOwned,
Expand All @@ -1715,48 +1713,11 @@ impl SymbolicationActor {
"unknown"
};
metric!(counter(metric) += 1, "reason" => reason);
if !perr.is_timeout() {
Self::save_minidump(minidump, minidump_cache)
.map_err(|e| log::error!("Failed to save minidump {:?}", &e))
.map(|r| {
if let Some(path) = r {
sentry::configure_scope(|scope| {
scope.set_extra(
"crashed_minidump",
sentry::protocol::Value::String(
path.to_string_lossy().to_string(),
),
);
});
}
})
.ok();
}
Err(anyhow::Error::new(perr))
}
}
}

/// Save a minidump to temporary location.
fn save_minidump(
minidump: &[u8],
failed_cache: crate::cache::Cache,
) -> anyhow::Result<Option<PathBuf>> {
if let Some(dir) = failed_cache.cache_dir() {
std::fs::create_dir_all(dir)?;
let tmp = tempfile::Builder::new()
.prefix("minidump")
.suffix(".dmp")
.tempfile_in(dir)?;
tmp.as_file().write_all(&*minidump)?;
let (_file, path) = tmp.keep().map_err(|e| e.error)?;
Ok(Some(path))
} else {
log::debug!("No diagnostics retention configured, not saving minidump");
Ok(None)
}
}

async fn load_cfi_caches(
&self,
scope: Scope,
Expand Down Expand Up @@ -1808,21 +1769,21 @@ impl SymbolicationActor {
/// modules.
async fn stackwalk_minidump_with_cfi(
&self,
minidump: Bytes,
minidump_file: &TempPath,
cfi_caches: &CfiCacheModules,
) -> Result<StackWalkMinidumpResult, anyhow::Error> {
) -> anyhow::Result<StackWalkMinidumpResult> {
let pool = self.spawnpool.clone();
let diagnostics_cache = self.diagnostics_cache.clone();
let cfi_caches = cfi_caches.for_processing();
let minidump_path = minidump_file.to_path_buf();
let lazy = async move {
let spawn_time = std::time::SystemTime::now();
let spawn_result = pool.spawn(
(
procspawn::serde::Json(cfi_caches),
minidump.clone(),
minidump_path,
spawn_time,
),
|(cfi_caches, minidump, spawn_time)| -> Result<_, ProcessMinidumpError> {
|(cfi_caches, minidump_path, spawn_time)| -> Result<_, ProcessMinidumpError> {
let procspawn::serde::Json(cfi_caches) = cfi_caches;

if let Ok(duration) = spawn_time.elapsed() {
Expand All @@ -1831,7 +1792,11 @@ impl SymbolicationActor {

// Stackwalk the minidump.
let cfi = load_cfi_for_processor(cfi_caches);
let minidump = ByteView::from_slice(&minidump);
// we cannot map an `io::Error` into `MinidumpNotFound` since there is no public
// constructor on `ProcessResult`. Passing in an empty buffer should result in
// the same error though.
let minidump =
ByteView::open(minidump_path).unwrap_or_else(|_| ByteView::from_slice(b""));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty buffer will now result in a generic "failed to read miniump" error with little information for debugging.

Is there no way you can wrap the error somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "minidump not found" is the appropriate error here. Also, not sure what to wrap this with, as also everything needs to go through serde, which std::io::Error does not.

let process_state = ProcessState::from_minidump(&minidump, Some(&cfi))?;
let minidump_state = MinidumpState::new(&process_state);
let object_type = minidump_state.object_type();
Expand Down Expand Up @@ -1910,8 +1875,6 @@ impl SymbolicationActor {
spawn_result,
Duration::from_secs(60),
"minidump.stackwalk.spawn.error",
&minidump,
diagnostics_cache,
)
};

Expand All @@ -1925,15 +1888,14 @@ impl SymbolicationActor {
async fn do_stackwalk_minidump(
self,
scope: Scope,
minidump: Vec<u8>,
minidump_file: TempPath,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> Result<(SymbolicateStacktraces, MinidumpState), SymbolicationError> {
let future = async move {
let minidump = Bytes::from(minidump);

log::debug!("Processing minidump ({} bytes)", minidump.len());
metric!(time_raw("minidump.upload.size") = minidump.len() as u64);
let len = minidump_file.metadata()?.len();
log::debug!("Processing minidump ({} bytes)", len);
metric!(time_raw("minidump.upload.size") = len);

let mut cfi_caches = CfiCacheModules::new();

Expand All @@ -1942,9 +1904,38 @@ impl SymbolicationActor {
let result = loop {
iterations += 1;

let result = self
.stackwalk_minidump_with_cfi(minidump.clone(), &cfi_caches)
.await?;
let result = match self
.stackwalk_minidump_with_cfi(&minidump_file, &cfi_caches)
.await
{
Ok(res) => res,
Err(err) => {
if let Some(dir) = self.diagnostics_cache.cache_dir() {
if let Some(file_name) = minidump_file.file_name() {
let path = dir.join(file_name);
match minidump_file.persist(&path) {
Comment on lines +1913 to +1916
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Temporary files cannot be persisted across filesystems.

Are we sure this is never going to be the case? It often is the case that /tmp is a different filesystem. And afaik our cache directory is a persistent volume so very likely to be a different filesystem to wherever tempfile defaults.

/cc @beezz: can you confirm whether this would work or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to save the files in $CACHE/tmp which is also used for other tmp files already, so that won’t run into this particular problem.

Ok(_) => {
sentry::configure_scope(|scope| {
scope.set_extra(
"crashed_minidump",
sentry::protocol::Value::String(
path.to_string_lossy().to_string(),
),
);
});
}
Err(e) => log::error!("Failed to save minidump {:?}", &e),
};
}
} else {
log::debug!("No diagnostics retention configured, not saving minidump");
}

// we explicitly match and return here, otherwise the borrow checker will
// complain that `minidump_file` is being moved into `save_minidump`
return Err(err);
}
};

let missing_modules: Vec<(CodeModuleId, &RawObjectInfo)> = result
.referenced_modules
Expand Down Expand Up @@ -2001,13 +1992,13 @@ impl SymbolicationActor {
async fn do_process_minidump(
self,
scope: Scope,
minidump: Vec<u8>,
minidump_file: TempPath,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> Result<CompletedSymbolicationResponse, SymbolicationError> {
let (request, state) = self
.clone()
.do_stackwalk_minidump(scope, minidump, sources, options)
.do_stackwalk_minidump(scope, minidump_file, sources, options)
.await?;

let mut response = self.do_symbolicate(request).await?;
Expand All @@ -2019,14 +2010,16 @@ impl SymbolicationActor {
pub fn process_minidump(
&self,
scope: Scope,
minidump: Vec<u8>,
minidump_file: TempPath,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> RequestId {
self.create_symbolication_request(
self.clone()
.do_process_minidump(scope, minidump, sources, options),
)
self.create_symbolication_request(self.clone().do_process_minidump(
scope,
minidump_file,
sources,
options,
))
}
}

Expand Down Expand Up @@ -2081,12 +2074,12 @@ impl SymbolicationActor {
async fn parse_apple_crash_report(
&self,
scope: Scope,
minidump: Vec<u8>,
report: File,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> Result<(SymbolicateStacktraces, AppleCrashReportState), SymbolicationError> {
let parse_future = async {
let report = AppleCrashReport::from_reader(Cursor::new(minidump))?;
let report = AppleCrashReport::from_reader(report)?;
let mut metadata = report.metadata;

let arch = report
Expand Down Expand Up @@ -2195,7 +2188,7 @@ impl SymbolicationActor {
async fn do_process_apple_crash_report(
self,
scope: Scope,
report: Vec<u8>,
report: File,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> Result<CompletedSymbolicationResponse, SymbolicationError> {
Expand All @@ -2211,7 +2204,7 @@ impl SymbolicationActor {
pub fn process_apple_crash_report(
&self,
scope: Scope,
apple_crash_report: Vec<u8>,
apple_crash_report: File,
sources: Arc<[SourceConfig]>,
options: RequestOptions,
) -> RequestId {
Expand Down Expand Up @@ -2282,13 +2275,16 @@ impl From<&SymCacheError> for ObjectFileStatus {

#[cfg(test)]
mod tests {
use tempfile::NamedTempFile;

use super::*;

use std::fs;
use std::io::Write;

use crate::config::Config;
use crate::services::Service;
use crate::test;
use crate::test::{self, fixture};

/// Setup tests and create a test service.
///
Expand Down Expand Up @@ -2476,11 +2472,13 @@ mod tests {
let (_symsrv, source) = test::symbol_server();

let minidump = test::read_fixture(path);
let mut minidump_file = NamedTempFile::new()?;
minidump_file.write_all(&minidump)?;
let symbolication = service.symbolication();
let response = test::spawn_compat(move || async move {
let request_id = symbolication.process_minidump(
Scope::Global,
minidump,
minidump_file.into_temp_path(),
Arc::new([source]),
RequestOptions {
dif_candidates: true,
Expand Down Expand Up @@ -2522,7 +2520,7 @@ mod tests {
let (service, _cache_dir) = setup_service();
let (_symsrv, source) = test::symbol_server();

let report_file = test::read_fixture("apple_crash_report.txt");
let report_file = std::fs::File::open(fixture("apple_crash_report.txt"))?;
let response = test::spawn_compat(move || async move {
let request_id = service.symbolication().process_apple_crash_report(
Scope::Global,
Expand Down
Loading