Skip to content

Commit 6abdc3d

Browse files
committed
controllers/krate/publish: Reduce spawn_blocking() scope
Quite a bit of code that is currently within the `spawn_blocking()` scope isn't using the sync database connection, so we might as well move it outside. One notable exception is the `process_tarball()` call, which stays within a dedicated `spawn_blocking()` call for now. While `process_tarball()` is operating on in-memory bytes and should be fairly fast, we sometimes process multiple megabytes of data here, so we can't rely on this being fast enough for an async thread.
1 parent d3106cd commit 6abdc3d

File tree

1 file changed

+137
-132
lines changed

1 file changed

+137
-132
lines changed

src/controllers/krate/publish.rs

Lines changed: 137 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -143,73 +143,69 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
143143
.check_rate_limit(auth.user().id, rate_limit_action, &mut conn)
144144
.await?;
145145

146-
spawn_blocking(move || {
147-
use diesel::RunQueryDsl;
148-
149-
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
150-
151-
let api_token_id = auth.api_token_id();
152-
let user = auth.user();
153-
154-
let content_length = tarball_bytes.len() as u64;
155-
156-
let maximums = Maximums::new(
157-
existing_crate.as_ref().and_then(|c| c.max_upload_size),
158-
app.config.max_upload_size,
159-
app.config.max_unpack_size,
160-
);
161-
162-
if content_length > maximums.max_upload_size {
163-
return Err(custom(StatusCode::PAYLOAD_TOO_LARGE, format!(
164-
"max upload size is: {}",
165-
maximums.max_upload_size
166-
)));
167-
}
146+
let content_length = tarball_bytes.len() as u64;
147+
148+
let maximums = Maximums::new(
149+
existing_crate.as_ref().and_then(|c| c.max_upload_size),
150+
app.config.max_upload_size,
151+
app.config.max_unpack_size,
152+
);
153+
154+
if content_length > maximums.max_upload_size {
155+
return Err(custom(
156+
StatusCode::PAYLOAD_TOO_LARGE,
157+
format!("max upload size is: {}", maximums.max_upload_size),
158+
));
159+
}
168160

161+
let tarball_info = spawn_blocking({
169162
let pkg_name = format!("{}-{}", &*metadata.name, &version_string);
170-
let tarball_info = process_tarball(&pkg_name, &*tarball_bytes, maximums.max_unpack_size)?;
171-
172-
// `unwrap()` is safe here since `process_tarball()` validates that
173-
// we only accept manifests with a `package` section and without
174-
// inheritance.
175-
let package = tarball_info.manifest.package.unwrap();
176-
177-
let description = package.description.map(|it| it.as_local().unwrap());
178-
let mut license = package.license.map(|it| it.as_local().unwrap());
179-
let license_file = package.license_file.map(|it| it.as_local().unwrap());
180-
let homepage = package.homepage.map(|it| it.as_local().unwrap());
181-
let documentation = package.documentation.map(|it| it.as_local().unwrap());
182-
let repository = package.repository.map(|it| it.as_local().unwrap());
183-
let rust_version = package.rust_version.map(|rv| rv.as_local().unwrap());
184-
let edition = package.edition.map(|rv| rv.as_local().unwrap());
185-
186-
// Make sure required fields are provided
187-
fn empty(s: Option<&String>) -> bool {
188-
s.map_or(true, String::is_empty)
189-
}
163+
let tarball_bytes = tarball_bytes.clone();
164+
move || process_tarball(&pkg_name, &*tarball_bytes, maximums.max_unpack_size)
165+
})
166+
.await??;
167+
168+
// `unwrap()` is safe here since `process_tarball()` validates that
169+
// we only accept manifests with a `package` section and without
170+
// inheritance.
171+
let package = tarball_info.manifest.package.unwrap();
172+
173+
let description = package.description.map(|it| it.as_local().unwrap());
174+
let mut license = package.license.map(|it| it.as_local().unwrap());
175+
let license_file = package.license_file.map(|it| it.as_local().unwrap());
176+
let homepage = package.homepage.map(|it| it.as_local().unwrap());
177+
let documentation = package.documentation.map(|it| it.as_local().unwrap());
178+
let repository = package.repository.map(|it| it.as_local().unwrap());
179+
let rust_version = package.rust_version.map(|rv| rv.as_local().unwrap());
180+
let edition = package.edition.map(|rv| rv.as_local().unwrap());
181+
182+
// Make sure required fields are provided
183+
fn empty(s: Option<&String>) -> bool {
184+
s.map_or(true, String::is_empty)
185+
}
190186

191-
// It can have up to three elements per below conditions.
192-
let mut missing = Vec::with_capacity(3);
193-
if empty(description.as_ref()) {
194-
missing.push("description");
195-
}
196-
if empty(license.as_ref()) && empty(license_file.as_ref()) {
197-
missing.push("license");
198-
}
199-
if !missing.is_empty() {
200-
let message = missing_metadata_error_message(&missing);
201-
return Err(bad_request(&message));
202-
}
187+
// It can have up to three elements per below conditions.
188+
let mut missing = Vec::with_capacity(3);
189+
if empty(description.as_ref()) {
190+
missing.push("description");
191+
}
192+
if empty(license.as_ref()) && empty(license_file.as_ref()) {
193+
missing.push("license");
194+
}
195+
if !missing.is_empty() {
196+
let message = missing_metadata_error_message(&missing);
197+
return Err(bad_request(&message));
198+
}
203199

204-
if let Some(description) = &description {
205-
if description.len() > MAX_DESCRIPTION_LENGTH {
206-
return Err(bad_request(format!("The `description` is too long. A maximum of {MAX_DESCRIPTION_LENGTH} characters are currently allowed.")));
207-
}
200+
if let Some(description) = &description {
201+
if description.len() > MAX_DESCRIPTION_LENGTH {
202+
return Err(bad_request(format!("The `description` is too long. A maximum of {MAX_DESCRIPTION_LENGTH} characters are currently allowed.")));
208203
}
204+
}
209205

210-
if let Some(ref license) = license {
211-
parse_license_expr(license).map_err(|e| bad_request(format_args!(
212-
"unknown or invalid license expression; \
206+
if let Some(ref license) = license {
207+
parse_license_expr(license).map_err(|e| bad_request(format_args!(
208+
"unknown or invalid license expression; \
213209
see http://opensource.org/licenses for options, \
214210
and http://spdx.org/licenses/ for their identifiers\n\
215211
Note: If you have a non-standard license that is not listed by SPDX, \
@@ -218,75 +214,76 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
218214
See https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields \
219215
for more information.\n\
220216
{e}"
221-
)))?;
222-
} else if license_file.is_some() {
223-
// If no license is given, but a license file is given, flag this
224-
// crate as having a nonstandard license. Note that we don't
225-
// actually do anything else with license_file currently.
226-
license = Some(String::from("non-standard"));
227-
}
217+
)))?;
218+
} else if license_file.is_some() {
219+
// If no license is given, but a license file is given, flag this
220+
// crate as having a nonstandard license. Note that we don't
221+
// actually do anything else with license_file currently.
222+
license = Some(String::from("non-standard"));
223+
}
228224

229-
validate_url(homepage.as_deref(), "homepage")?;
230-
validate_url(documentation.as_deref(), "documentation")?;
231-
validate_url(repository.as_deref(), "repository")?;
232-
if let Some(ref rust_version) = rust_version {
233-
validate_rust_version(rust_version)?;
234-
}
225+
validate_url(homepage.as_deref(), "homepage")?;
226+
validate_url(documentation.as_deref(), "documentation")?;
227+
validate_url(repository.as_deref(), "repository")?;
228+
if let Some(ref rust_version) = rust_version {
229+
validate_rust_version(rust_version)?;
230+
}
235231

236-
let keywords = package
237-
.keywords
238-
.map(|it| it.as_local().unwrap())
239-
.unwrap_or_default();
232+
let keywords = package
233+
.keywords
234+
.map(|it| it.as_local().unwrap())
235+
.unwrap_or_default();
240236

241-
if keywords.len() > 5 {
242-
return Err(bad_request("expected at most 5 keywords per crate"));
243-
}
237+
if keywords.len() > 5 {
238+
return Err(bad_request("expected at most 5 keywords per crate"));
239+
}
244240

245-
for keyword in keywords.iter() {
246-
if keyword.len() > 20 {
247-
return Err(bad_request(format!(
248-
"\"{keyword}\" is an invalid keyword (keywords must have less than 20 characters)"
249-
)));
250-
} else if !Keyword::valid_name(keyword) {
251-
return Err(bad_request(format!("\"{keyword}\" is an invalid keyword")));
252-
}
241+
for keyword in keywords.iter() {
242+
if keyword.len() > 20 {
243+
return Err(bad_request(format!(
244+
"\"{keyword}\" is an invalid keyword (keywords must have less than 20 characters)"
245+
)));
246+
} else if !Keyword::valid_name(keyword) {
247+
return Err(bad_request(format!("\"{keyword}\" is an invalid keyword")));
253248
}
249+
}
254250

255-
let categories = package
256-
.categories
257-
.map(|it| it.as_local().unwrap())
258-
.unwrap_or_default();
251+
let categories = package
252+
.categories
253+
.map(|it| it.as_local().unwrap())
254+
.unwrap_or_default();
259255

260-
if categories.len() > 5 {
261-
return Err(bad_request("expected at most 5 categories per crate"));
262-
}
256+
if categories.len() > 5 {
257+
return Err(bad_request("expected at most 5 categories per crate"));
258+
}
263259

264-
let max_features = existing_crate.as_ref()
265-
.and_then(|c| c.max_features.map(|mf| mf as usize))
266-
.unwrap_or(app.config.max_features);
260+
let max_features = existing_crate
261+
.as_ref()
262+
.and_then(|c| c.max_features.map(|mf| mf as usize))
263+
.unwrap_or(app.config.max_features);
267264

268-
let features = tarball_info.manifest.features.unwrap_or_default();
269-
let num_features = features.len();
270-
if num_features > max_features {
271-
return Err(bad_request(format!(
272-
"crates.io only allows a maximum number of {max_features} \
265+
let features = tarball_info.manifest.features.unwrap_or_default();
266+
let num_features = features.len();
267+
if num_features > max_features {
268+
return Err(bad_request(format!(
269+
"crates.io only allows a maximum number of {max_features} \
273270
features, but your crate is declaring {num_features} features.\n\
274271
\n\
275272
Take a look at https://blog.rust-lang.org/2023/10/26/broken-badges-and-23k-keywords.html \
276273
to understand why this restriction was introduced.\n\
277274
\n\
278275
If you have a use case that requires an increase of this limit, \
279276
please send us an email to [email protected] to discuss the details."
280-
)));
281-
}
277+
)));
278+
}
282279

283-
for (key, values) in features.iter() {
284-
Crate::validate_feature_name(key).map_err(bad_request)?;
280+
for (key, values) in features.iter() {
281+
Crate::validate_feature_name(key).map_err(bad_request)?;
285282

286-
let num_features = values.len();
287-
if num_features > max_features {
288-
return Err(bad_request(format!(
289-
"crates.io only allows a maximum number of {max_features} \
283+
let num_features = values.len();
284+
if num_features > max_features {
285+
return Err(bad_request(format!(
286+
"crates.io only allows a maximum number of {max_features} \
290287
features or dependencies that another feature can enable, \
291288
but the \"{key}\" feature of your crate is enabling \
292289
{num_features} features or dependencies.\n\
@@ -296,34 +293,42 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
296293
\n\
297294
If you have a use case that requires an increase of this limit, \
298295
please send us an email to [email protected] to discuss the details."
299-
)));
300-
}
296+
)));
297+
}
301298

302-
for value in values.iter() {
303-
Crate::validate_feature(value).map_err(bad_request)?;
304-
}
299+
for value in values.iter() {
300+
Crate::validate_feature(value).map_err(bad_request)?;
305301
}
302+
}
306303

307-
let deps = convert_dependencies(
308-
tarball_info.manifest.dependencies.as_ref(),
309-
tarball_info.manifest.dev_dependencies.as_ref(),
310-
tarball_info.manifest.build_dependencies.as_ref(),
311-
tarball_info.manifest.target.as_ref()
312-
);
304+
let deps = convert_dependencies(
305+
tarball_info.manifest.dependencies.as_ref(),
306+
tarball_info.manifest.dev_dependencies.as_ref(),
307+
tarball_info.manifest.build_dependencies.as_ref(),
308+
tarball_info.manifest.target.as_ref(),
309+
);
313310

314-
let max_dependencies = app.config.max_dependencies;
315-
if deps.len() > max_dependencies {
316-
return Err(bad_request(format!(
317-
"crates.io only allows a maximum number of {max_dependencies} dependencies.\n\
311+
let max_dependencies = app.config.max_dependencies;
312+
if deps.len() > max_dependencies {
313+
return Err(bad_request(format!(
314+
"crates.io only allows a maximum number of {max_dependencies} dependencies.\n\
318315
\n\
319316
If you have a use case that requires an increase of this limit, \
320317
please send us an email to [email protected] to discuss the details."
321-
)));
322-
}
318+
)));
319+
}
323320

324-
for dep in &deps {
325-
validate_dependency(dep)?;
326-
}
321+
for dep in &deps {
322+
validate_dependency(dep)?;
323+
}
324+
325+
spawn_blocking(move || {
326+
use diesel::RunQueryDsl;
327+
328+
let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into();
329+
330+
let api_token_id = auth.api_token_id();
331+
let user = auth.user();
327332

328333
// Create a transaction on the database, if there are no errors,
329334
// commit the transactions to record a new or updated crate.

0 commit comments

Comments
 (0)