Skip to content

Commit bc3f03f

Browse files
NathanFlurryMasterPtato
authored andcommitted
chore(pegboard): send artifact image size from workflow instead of fetching with HEAD
1 parent fad23d9 commit bc3f03f

7 files changed

Lines changed: 84 additions & 101 deletions

File tree

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.

packages/edge/api/intercom/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ rivet-env.workspace = true
2626
rivet-health-checks.workspace = true
2727
rivet-operation.workspace = true
2828
rivet-pools.workspace = true
29+
upload-get.workspace = true
2930
s3-util.workspace = true
3031
serde = { version = "1.0", features = ["derive"] }
3132
serde_json = "1.0"

packages/edge/api/intercom/src/route/pegboard.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,19 @@ pub async fn prewarm_image(
7171
let dc = unwrap!(dc_res.datacenters.first());
7272
let build = unwrap!(builds_res.builds.first());
7373

74-
let fallback_artifact_url =
75-
resolve_image_fallback_artifact_url(&ctx, dc.build_delivery_method, &build).await?;
74+
// Only prewarm if using ATS
75+
let BuildDeliveryMethod::TrafficServer = dc.build_delivery_method else {
76+
tracing::debug!("skipping prewarm since we're not using ats build delivery method");
77+
return Ok(json!({}));
78+
};
79+
80+
// Get the artifact size
81+
let uploads_res = op!([ctx] upload_get {
82+
upload_ids: vec![build.upload_id.into()],
83+
})
84+
.await?;
85+
let upload = unwrap!(uploads_res.uploads.first());
86+
let artifact_size_bytes = upload.content_length;
7687

7788
let res = ctx
7889
.signal(pegboard::workflows::client::PrewarmImage2 {
@@ -83,7 +94,10 @@ pub async fn prewarm_image(
8394
build.upload_id,
8495
&build::utils::file_name(build.kind, build.compression),
8596
)?,
86-
fallback_artifact_url,
97+
// We will never need to fall back to fetching directly from S3. This short
98+
// circuits earlier in the fn.
99+
fallback_artifact_url: None,
100+
artifact_size_bytes,
87101
kind: build.kind.into(),
88102
compression: build.compression.into(),
89103
},

packages/edge/infra/client/manager/src/image_download_handler.rs

Lines changed: 22 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -99,28 +99,28 @@ impl ImageDownloadHandler {
9999
let mut conn = ctx.sql().await?;
100100
let mut tx = conn.begin().await?;
101101

102-
let ((cache_count, images_dir_size), image_download_size) = tokio::try_join!(
103-
async {
104-
// Get total size of images directory. Note that it doesn't matter if this doesn't
105-
// match the actual fs size because it should either be exactly at or below actual fs
106-
// size. Also calculating fs size manually is expensive.
107-
sqlx::query_as::<_, (i64, i64)>(indoc!(
108-
"
109-
SELECT COUNT(size), COALESCE(SUM(size), 0) FROM images_cache
110-
",
111-
))
112-
.fetch_one(&mut *tx)
113-
.await
114-
.map_err(Into::<anyhow::Error>::into)
115-
},
116-
// NOTE: The image size here is somewhat misleading because its only the size of the
117-
// downloaded archive and not the total disk usage after it is unpacked. However, this is
118-
// good enough
119-
self.fetch_image_download_size(ctx, image_config),
120-
)?;
102+
// Get total size of images directory. Note that it doesn't matter if this doesn't
103+
// match the actual fs size because it should either be exactly at or below actual fs
104+
// size. Also calculating fs size manually is expensive.
105+
let (cache_count, images_dir_size) = sqlx::query_as::<_, (i64, i64)>(indoc!(
106+
"
107+
SELECT COUNT(size), COALESCE(SUM(size), 0) FROM images_cache
108+
",
109+
))
110+
.fetch_one(&mut *tx)
111+
.await
112+
.map_err(Into::<anyhow::Error>::into)?;
121113

122114
// Prune images
123-
let (removed_count, removed_bytes) = if images_dir_size as u64 + image_download_size
115+
//
116+
// HACK: The artifact_size_bytes here is somewhat misleading because its only the size of the
117+
// downloaded archive and not the total disk usage after it is unpacked. However, this is size
118+
// is recalculated later once decompressed, so this will only ever exceed the cache
119+
// size limit in edge cases by `actual size - compressed size`. In this situation,
120+
// that extra difference is already reserved on the file system by the actor
121+
// itself.
122+
let (removed_count, removed_bytes) = if images_dir_size as u64
123+
+ image_config.artifact_size_bytes
124124
> ctx.config().images.max_cache_size()
125125
{
126126
// Fetch as many images as it takes to clear up enough space for this new image.
@@ -157,7 +157,7 @@ impl ImageDownloadHandler {
157157
.bind(image_config.id)
158158
.bind(
159159
(images_dir_size as u64)
160-
.saturating_add(image_download_size)
160+
.saturating_add(image_config.artifact_size_bytes)
161161
.saturating_sub(ctx.config().images.max_cache_size()) as i64,
162162
)
163163
.fetch_all(&mut *tx)
@@ -202,7 +202,7 @@ impl ImageDownloadHandler {
202202

203203
metrics::IMAGE_CACHE_COUNT.set(cache_count + 1 - removed_count);
204204
metrics::IMAGE_CACHE_SIZE
205-
.set(images_dir_size + image_download_size as i64 - removed_bytes);
205+
.set(images_dir_size + image_config.artifact_size_bytes as i64 - removed_bytes);
206206

207207
sqlx::query(indoc!(
208208
"
@@ -487,51 +487,4 @@ impl ImageDownloadHandler {
487487

488488
Ok(addresses)
489489
}
490-
491-
/// Attempts to fetch HEAD for the image download url and determine the image's download size.
492-
async fn fetch_image_download_size(
493-
&self,
494-
ctx: &Ctx,
495-
image_config: &protocol::Image,
496-
) -> Result<u64> {
497-
let addresses = self.get_image_addresses(ctx, image_config).await?;
498-
499-
let mut iter = addresses.into_iter();
500-
while let Some(artifact_url) = iter.next() {
501-
// Log the full URL we're attempting to download from
502-
tracing::info!(image_id=?image_config.id, %artifact_url, "attempting to download image");
503-
504-
match reqwest::Client::new()
505-
.head(&artifact_url)
506-
.send()
507-
.await
508-
.and_then(|res| res.error_for_status())
509-
{
510-
Ok(res) => {
511-
tracing::info!(image_id=?image_config.id, %artifact_url, "successfully fetched image HEAD");
512-
513-
// Read Content-Length header from response
514-
let image_size = res
515-
.headers()
516-
.get(reqwest::header::CONTENT_LENGTH)
517-
.context("no Content-Length header")?
518-
.to_str()?
519-
.parse::<u64>()
520-
.context("invalid Content-Length header")?;
521-
522-
return Ok(image_size);
523-
}
524-
Err(err) => {
525-
tracing::warn!(
526-
image_id=?image_config.id,
527-
%artifact_url,
528-
%err,
529-
"failed to fetch image HEAD"
530-
);
531-
}
532-
}
533-
}
534-
535-
bail!("artifact url could not be resolved");
536-
}
537490
}

packages/edge/services/pegboard/src/protocol.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ pub struct Image {
117117
pub artifact_url_stub: String,
118118
/// Direct S3 url to download the image from without ATS.
119119
pub fallback_artifact_url: Option<String>,
120+
/// Size in bytes of the artfiact.
121+
pub artifact_size_bytes: u64,
120122
pub kind: ImageKind,
121123
pub compression: ImageCompression,
122124
}

packages/edge/services/pegboard/src/workflows/actor/runtime.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use foundationdb::{
88
options::{ConflictRangeType, StreamingMode},
99
};
1010
use futures_util::{FutureExt, TryStreamExt};
11+
use rivet_api::models::actors_endpoint_type;
1112
use sqlx::Acquire;
1213

1314
use super::{
@@ -685,6 +686,7 @@ pub async fn spawn_actor(
685686
id: actor_setup.image_id,
686687
artifact_url_stub: actor_setup.artifact_url_stub.clone(),
687688
fallback_artifact_url: actor_setup.fallback_artifact_url.clone(),
689+
artifact_size_bytes: actor_setup.artifact_size_bytes,
688690
kind: actor_setup.meta.build_kind.into(),
689691
compression: actor_setup.meta.build_compression.into(),
690692
},

packages/edge/services/pegboard/src/workflows/actor/setup.rs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ pub struct ActorSetupCtx {
559559
pub resources: protocol::Resources,
560560
pub artifact_url_stub: String,
561561
pub fallback_artifact_url: Option<String>,
562+
pub artifact_size_bytes: u64,
562563
}
563564

564565
pub async fn setup(
@@ -630,6 +631,7 @@ pub async fn setup(
630631
resources,
631632
artifact_url_stub: artifacts_res.artifact_url_stub,
632633
fallback_artifact_url: artifacts_res.fallback_artifact_url,
634+
artifact_size_bytes: artifacts_res.artifact_size_bytes,
633635
})
634636
}
635637

@@ -707,47 +709,54 @@ struct ResolveArtifactsInput {
707709
struct ResolveArtifactsOutput {
708710
artifact_url_stub: String,
709711
fallback_artifact_url: Option<String>,
712+
artifact_size_bytes: u64,
710713
}
711714

712715
#[activity(ResolveArtifacts)]
713716
async fn resolve_artifacts(
714717
ctx: &ActivityCtx,
715718
input: &ResolveArtifactsInput,
716719
) -> GlobalResult<ResolveArtifactsOutput> {
717-
let fallback_artifact_url =
718-
if let BuildDeliveryMethod::S3Direct = input.dc_build_delivery_method {
719-
tracing::debug!("using s3 direct delivery");
720-
721-
// Build client
722-
let s3_client = s3_util::Client::with_bucket_and_endpoint(
723-
ctx.config(),
724-
"bucket-build",
725-
s3_util::EndpointKind::EdgeInternal,
720+
// Get the fallback URL
721+
let fallback_artifact_url = {
722+
tracing::debug!("using s3 direct delivery");
723+
724+
// Build client
725+
let s3_client = s3_util::Client::with_bucket_and_endpoint(
726+
ctx.config(),
727+
"bucket-build",
728+
s3_util::EndpointKind::EdgeInternal,
729+
)
730+
.await?;
731+
732+
let presigned_req = s3_client
733+
.get_object()
734+
.bucket(s3_client.bucket())
735+
.key(format!(
736+
"{upload_id}/{file_name}",
737+
upload_id = input.build_upload_id,
738+
file_name = input.build_file_name,
739+
))
740+
.presigned(
741+
s3_util::aws_sdk_s3::presigning::PresigningConfig::builder()
742+
.expires_in(std::time::Duration::from_secs(15 * 60))
743+
.build()?,
726744
)
727745
.await?;
728746

729-
let presigned_req = s3_client
730-
.get_object()
731-
.bucket(s3_client.bucket())
732-
.key(format!(
733-
"{upload_id}/{file_name}",
734-
upload_id = input.build_upload_id,
735-
file_name = input.build_file_name,
736-
))
737-
.presigned(
738-
s3_util::aws_sdk_s3::presigning::PresigningConfig::builder()
739-
.expires_in(std::time::Duration::from_secs(15 * 60))
740-
.build()?,
741-
)
742-
.await?;
747+
let addr_str = presigned_req.uri().to_string();
748+
tracing::debug!(addr = %addr_str, "resolved artifact s3 presigned request");
743749

744-
let addr_str = presigned_req.uri().to_string();
745-
tracing::debug!(addr = %addr_str, "resolved artifact s3 presigned request");
750+
Some(addr_str)
751+
};
746752

747-
Some(addr_str)
748-
} else {
749-
None
750-
};
753+
// Get the artifact size
754+
let uploads_res = op!([ctx] upload_get {
755+
upload_ids: vec![input.build_upload_id.into()],
756+
})
757+
.await?;
758+
let upload = unwrap!(uploads_res.uploads.first());
759+
let artifact_size_bytes = upload.content_length;
751760

752761
Ok(ResolveArtifactsOutput {
753762
artifact_url_stub: crate::util::image_artifact_url_stub(
@@ -756,5 +765,6 @@ async fn resolve_artifacts(
756765
&input.build_file_name,
757766
)?,
758767
fallback_artifact_url,
768+
artifact_size_bytes,
759769
})
760770
}

0 commit comments

Comments
 (0)