Skip to content

Commit

Permalink
feat(proguard): Introduce experimental chunk uploading feature
Browse files Browse the repository at this point in the history
Introduce an experimental chunk uploading feature for the `sentry-cli upload-proguard` command. The feature can be activated by setting the `SENTRY_EXPERIMENTAL_PROGUARD_CHUNK_UPLOAD` environment variable to `1`. The feature is only activated when users opt in via this environment variable.

The experimental chunk uploading feature is not backwards compatible. We attempt the upload regardless of whether the server supports receiving chunk-uploaded Proguard files. Server-side support will only be available once getsentry/sentry#81131 is released. The goal here was to create something that works, so some optimizations that we use for other chunk uploaded file types (e.g. first checking whether any chunks are present on the server, and only uploading the missing ones), are not implemented for Proguard.

Ref #2196
  • Loading branch information
szokeasaurusrex committed Nov 21, 2024
1 parent 992118c commit 4c62a8e
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 49 deletions.
125 changes: 76 additions & 49 deletions src/commands/upload_proguard.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::env;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;

use anyhow::{bail, Error, Result};
Expand All @@ -17,16 +19,23 @@ use crate::config::Config;
use crate::utils::android::dump_proguard_uuids_as_properties;
use crate::utils::args::ArgExt;
use crate::utils::fs::TempFile;
use crate::utils::proguard_upload;
use crate::utils::system::QuietExit;
use crate::utils::ui::{copy_with_progress, make_byte_progress_bar};

#[derive(Debug)]
struct MappingRef {
pub struct MappingRef {
pub path: PathBuf,
pub size: u64,
pub uuid: Uuid,
}

impl AsRef<Path> for MappingRef {
fn as_ref(&self) -> &Path {
&self.path
}
}

pub fn make_command(command: Command) -> Command {
command
.about("Upload ProGuard mapping files to a project.")
Expand Down Expand Up @@ -188,62 +197,80 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
}
}

if mappings.is_empty() && matches.get_flag("require_one") {
println!();
eprintln!("{}", style("error: found no mapping files to upload").red());
return Err(QuietExit(1).into());
}
let api = Api::current();
let authenticated_api = api.authenticated()?;
let config = Config::current();
let (org, project) = config.get_org_and_project(matches)?;

println!("{} compressing mappings", style(">").dim());
let tf = TempFile::create()?;
{
let mut zip = zip::ZipWriter::new(tf.open()?);
for mapping in &mappings {
let pb = make_byte_progress_bar(mapping.size);
zip.start_file(
format!("proguard/{}.txt", mapping.uuid),
zip::write::FileOptions::default(),
)?;
copy_with_progress(&pb, &mut fs::File::open(&mapping.path)?, &mut zip)?;
pb.finish_and_clear();
let chunk_upload_options =
if env::var("SENTRY_EXPERIMENTAL_PROGUARD_CHUNK_UPLOAD") == Ok("1".into()) {
authenticated_api.get_chunk_upload_options(&org)?
} else {
// Only attempt chunk uploading if env var is set.
None
};

if let Some(chunk_upload_options) = chunk_upload_options {
log::warn!(
"EXPERIMENTAL FEATURE: Uploading proguard mappings using chunked uploading. \
Some functionality may be unavailable when using chunked uploading. Please unset \
the SENTRY_EXPERIMENTAL_PROGUARD_CHUNK_UPLOAD variable if you encounter any \
problems."
);
proguard_upload::chunk_upload(&mappings, &chunk_upload_options, &org, &project)?;
} else {
if mappings.is_empty() && matches.get_flag("require_one") {
println!();
eprintln!("{}", style("error: found no mapping files to upload").red());
return Err(QuietExit(1).into());
}
}

// write UUIDs into the mapping file.
if let Some(p) = matches.get_one::<String>("write_properties") {
let uuids: Vec<_> = mappings.iter().map(|x| x.uuid).collect();
dump_proguard_uuids_as_properties(p, &uuids)?;
}
println!("{} compressing mappings", style(">").dim());
let tf = TempFile::create()?;
{
let mut zip = zip::ZipWriter::new(tf.open()?);
for mapping in &mappings {
let pb = make_byte_progress_bar(mapping.size);
zip.start_file(
format!("proguard/{}.txt", mapping.uuid),
zip::write::FileOptions::default(),
)?;
copy_with_progress(&pb, &mut fs::File::open(&mapping.path)?, &mut zip)?;
pb.finish_and_clear();
}
}

if matches.get_flag("no_upload") {
println!("{} skipping upload.", style(">").dim());
return Ok(());
}
// write UUIDs into the mapping file.
if let Some(p) = matches.get_one::<String>("write_properties") {
let uuids: Vec<_> = mappings.iter().map(|x| x.uuid).collect();
dump_proguard_uuids_as_properties(p, &uuids)?;
}

println!("{} uploading mappings", style(">").dim());
let config = Config::current();
let (org, project) = config.get_org_and_project(matches)?;
if matches.get_flag("no_upload") {
println!("{} skipping upload.", style(">").dim());
return Ok(());
}

info!(
"Issuing a command for Organization: {} Project: {}",
org, project
);
println!("{} uploading mappings", style(">").dim());

let api = Api::current();
let authenticated_api = api.authenticated()?;
info!(
"Issuing a command for Organization: {} Project: {}",
org, project
);

let rv = authenticated_api
.region_specific(&org)
.upload_dif_archive(&project, tf.path())?;
println!(
"{} Uploaded a total of {} new mapping files",
style(">").dim(),
style(rv.len()).yellow()
);
if !rv.is_empty() {
println!("Newly uploaded debug symbols:");
for df in rv {
println!(" {}", style(&df.id()).dim());
let rv = authenticated_api
.region_specific(&org)
.upload_dif_archive(&project, tf.path())?;
println!(
"{} Uploaded a total of {} new mapping files",
style(">").dim(),
style(rv.len()).yellow()
);
if !rv.is_empty() {
println!("Newly uploaded debug symbols:");
for df in rv {
println!(" {}", style(&df.id()).dim());
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod http;
pub mod logging;
pub mod metrics;
pub mod progress;
pub mod proguard_upload;
pub mod releases;
pub mod retry;
pub mod sourcemaps;
Expand Down
123 changes: 123 additions & 0 deletions src/utils/proguard_upload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! This file contains code for enabling chunk uploads for Proguard mappings.
//!
//! This code is intended as a temporary solution to enable chunk uploads for
//! Proguard mappings, while we work on a more permanent solution, which will
//! work for all different types of debug files.

use std::{fs, thread, time::Duration};

use anyhow::{bail, Result};
use indicatif::ProgressStyle;
use sha1_smol::Digest;

use super::chunks;
use super::chunks::Chunk;
use super::fs::get_sha1_checksums;
use crate::api::{Api, ChunkUploadOptions, ChunkedDifRequest, ChunkedFileState};
use crate::commands::upload_proguard::MappingRef;

struct ChunkedMapping {
raw_data: Vec<u8>,
hash: Digest,
chunk_hashes: Vec<Digest>,
file_name: String,
chunk_size: usize,
}

impl ChunkedMapping {
fn try_from_mapping(mapping: &MappingRef, chunk_size: u64) -> Result<Self> {
let raw_data = fs::read(mapping)?;
let file_name = format!("/proguard/{}.txt", mapping.uuid);

let (hash, chunk_hashes) = get_sha1_checksums(&raw_data, chunk_size)?;
println!("There are {} chunks", chunk_hashes.len());
Ok(Self {
raw_data,
hash,
chunk_hashes,
file_name,
chunk_size: chunk_size.try_into()?,
})
}

fn chunks(&self) -> impl Iterator<Item = Chunk<'_>> {
self.raw_data
.chunks(self.chunk_size)
.zip(self.chunk_hashes.iter())
.map(|(chunk, hash)| Chunk((*hash, chunk)))
}
}

impl<'a> From<&'a ChunkedMapping> for ChunkedDifRequest<'a> {
fn from(value: &'a ChunkedMapping) -> Self {
ChunkedDifRequest {
name: &value.file_name,
debug_id: None,
chunks: &value.chunk_hashes,
}
}
}

fn to_assemble(chunked: &ChunkedMapping) -> (Digest, ChunkedDifRequest<'_>) {
(chunked.hash, chunked.into())
}

pub fn chunk_upload(
paths: &[MappingRef],
chunk_upload_options: &ChunkUploadOptions,
org: &str,
project: &str,
) -> Result<()> {
let chunked_mappings: Vec<ChunkedMapping> = paths
.iter()
.map(|path| ChunkedMapping::try_from_mapping(path, chunk_upload_options.chunk_size))
.collect::<Result<_>>()?;

let progress_style = ProgressStyle::default_bar().template(&format!(
"Uploading Proguard mappings...\
\n{{wide_bar}} {{bytes}}/{{total_bytes}} ({{eta}})"
));

let chunks = chunked_mappings.iter().flat_map(|mapping| mapping.chunks());

chunks::upload_chunks(
&chunks.collect::<Vec<_>>(),
chunk_upload_options,
progress_style,
)?;

let request = chunked_mappings.iter().map(to_assemble).collect();
loop {
let okay = Api::current()
.authenticated()?
.assemble_difs(org, project, &request)?
.iter()
.map(|(_, response)| {
Ok(match response.state {
ChunkedFileState::Error => bail!(format!("Error: {:?}", response)),
ChunkedFileState::NotFound => bail!("File not found"),
ChunkedFileState::Created => {
println!("Created");
false
}
ChunkedFileState::Assembling => {
println!("Assembling");
false
}
ChunkedFileState::Ok => true,
})
})
.collect::<Result<Vec<_>>>()?
.iter()
.all(|&done| done);

if okay {
println!("Uploaded");
break;
}

thread::sleep(Duration::from_secs(1));
}

Ok(())
}

0 comments on commit 4c62a8e

Please sign in to comment.