Skip to content

Commit f1b7d04

Browse files
committed
crates-admin: Add backfill-cache-tags command
This adds a `backfill-cache-tags` subcommand that queues a `BackfillCacheTags` job per crate. `--backfill` selects every crate without a completed `cache_tags_backfills` row. Passing crate names instead queues just those. This is temporary scaffolding and should be removed once the backfill is complete.
1 parent a798ac6 commit f1b7d04

2 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use anyhow::Context;
2+
use crates_io::config::FeaturesConfig;
3+
use crates_io::db;
4+
use crates_io::schema::background_jobs;
5+
use crates_io::schema::{cache_tags_backfills, crates};
6+
use crates_io::worker::jobs::BackfillCacheTags;
7+
use crates_io_worker::BackgroundJob;
8+
use diesel::prelude::*;
9+
use diesel_async::{AsyncPgConnection, RunQueryDsl};
10+
use indicatif::{ProgressBar, ProgressStyle};
11+
12+
const CHUNK_SIZE: usize = 100;
13+
14+
/// Priority for the queued jobs. Negative so the one-time bulk backfill yields
15+
/// to regular background work.
16+
const PRIORITY: i16 = -50;
17+
18+
#[derive(clap::Parser, Debug, Eq, PartialEq)]
19+
#[clap(
20+
name = "backfill-cache-tags",
21+
about = "Queue background jobs to backfill `cache-tags` metadata onto a crate's S3 objects.",
22+
group(clap::ArgGroup::new("mode").required(true))
23+
)]
24+
pub struct Options {
25+
/// Backfill *all* crates that have not yet been backfilled.
26+
#[clap(long, group = "mode")]
27+
backfill: bool,
28+
29+
/// Names of the crates to backfill.
30+
#[clap(group = "mode")]
31+
crates: Vec<String>,
32+
}
33+
34+
pub async fn run(opts: Options) -> anyhow::Result<()> {
35+
let features = FeaturesConfig::from_env().context("Failed to load features config")?;
36+
if !features.cache_tags_enabled {
37+
println!("`CACHE_TAGS_ENABLED` is not set, skipping backfill");
38+
return Ok(());
39+
}
40+
41+
let conn = db::oneoff_connection().await;
42+
let mut conn = conn.context("Failed to connect to the database")?;
43+
44+
let names = load_crate_names(&opts, &conn).await;
45+
let names = names.context("Failed to load crates")?;
46+
if names.is_empty() {
47+
println!("No matching crates found.");
48+
return Ok(());
49+
}
50+
51+
println!("Found {} matching crates", names.len());
52+
53+
let pb_style = ProgressStyle::with_template("{bar:60} ({pos}/{len}, ETA {eta})")?;
54+
let pb = ProgressBar::new(names.len() as u64).with_style(pb_style);
55+
56+
let mut queued_count = 0;
57+
let mut error_count = 0;
58+
59+
for batch in names.chunks(CHUNK_SIZE) {
60+
let jobs = batch
61+
.iter()
62+
.map(|name| NewBackgroundJob::new(name))
63+
.collect::<anyhow::Result<Vec<_>>>()?;
64+
65+
let num_jobs = jobs.len();
66+
67+
let result = diesel::insert_into(background_jobs::table)
68+
.values(&jobs)
69+
.execute(&mut conn)
70+
.await;
71+
72+
pb.inc(num_jobs as u64);
73+
74+
if let Err(err) = result {
75+
error_count += num_jobs;
76+
pb.suspend(|| eprintln!("Failed to queue background jobs: {err}"));
77+
} else {
78+
queued_count += num_jobs;
79+
}
80+
}
81+
82+
pb.finish_with_message("Done");
83+
84+
println!("Successfully queued {queued_count} jobs");
85+
if error_count > 0 {
86+
println!("Failed to queue {error_count} jobs");
87+
}
88+
89+
Ok(())
90+
}
91+
92+
async fn load_crate_names(
93+
opts: &Options,
94+
mut conn: &AsyncPgConnection,
95+
) -> QueryResult<Vec<String>> {
96+
let mut query = crates::table
97+
.left_join(
98+
cache_tags_backfills::table
99+
.on(cache_tags_backfills::crate_id.eq(crates::id.nullable())),
100+
)
101+
.select(crates::name)
102+
.into_boxed();
103+
104+
if opts.backfill {
105+
query = query.filter(cache_tags_backfills::id.is_null());
106+
} else {
107+
query = query.filter(crates::name.eq_any(&opts.crates));
108+
}
109+
110+
query.load(&mut conn).await
111+
}
112+
113+
/// Represents a new background job to be inserted into the database.
114+
#[derive(Debug, diesel::Insertable)]
115+
#[diesel(table_name = background_jobs)]
116+
struct NewBackgroundJob {
117+
job_type: &'static str,
118+
data: serde_json::Value,
119+
priority: i16,
120+
}
121+
122+
impl NewBackgroundJob {
123+
/// Creates a new [`BackfillCacheTags`] background job for the given crate.
124+
fn new(name: &str) -> anyhow::Result<Self> {
125+
let job = BackfillCacheTags::new(name.to_string());
126+
let data = serde_json::to_value(&job).context("Failed to serialize job data")?;
127+
128+
Ok(Self {
129+
job_type: BackfillCacheTags::JOB_NAME,
130+
data,
131+
priority: PRIORITY,
132+
})
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
use crates_io::schema::cache_tags_backfills;
140+
use crates_io_database::models::{NewCacheTagsBackfillRow, NewUser};
141+
use crates_io_test_db::TestDatabase;
142+
use crates_io_test_utils::builders::CrateBuilder;
143+
144+
async fn create_user(conn: &AsyncPgConnection) -> i32 {
145+
NewUser::builder()
146+
.gh_id(1)
147+
.gh_login("testuser")
148+
.username("testuser")
149+
.gh_encrypted_token(&[])
150+
.build()
151+
.insert(conn)
152+
.await
153+
.unwrap()
154+
}
155+
156+
fn opts(backfill: bool, crates: &[&str]) -> Options {
157+
Options {
158+
backfill,
159+
crates: crates.iter().map(|name| name.to_string()).collect(),
160+
}
161+
}
162+
163+
#[tokio::test]
164+
async fn load_crate_names_returns_named_crates() {
165+
let test_db = TestDatabase::new();
166+
let mut conn = test_db.async_connect().await;
167+
let user_id = create_user(&conn).await;
168+
169+
CrateBuilder::new("foo", user_id)
170+
.expect_build(&mut conn)
171+
.await;
172+
173+
CrateBuilder::new("bar", user_id)
174+
.expect_build(&mut conn)
175+
.await;
176+
177+
let names = load_crate_names(&opts(false, &["foo"]), &conn)
178+
.await
179+
.unwrap();
180+
181+
assert_eq!(names, vec!["foo"]);
182+
}
183+
184+
#[tokio::test]
185+
async fn load_crate_names_backfill_excludes_completed_crates() {
186+
let test_db = TestDatabase::new();
187+
let mut conn = test_db.async_connect().await;
188+
let user_id = create_user(&conn).await;
189+
190+
let foo = CrateBuilder::new("foo", user_id)
191+
.expect_build(&mut conn)
192+
.await;
193+
194+
CrateBuilder::new("bar", user_id)
195+
.expect_build(&mut conn)
196+
.await;
197+
198+
let row = NewCacheTagsBackfillRow::builder()
199+
.crate_id(foo.id)
200+
.crate_name("foo")
201+
.build();
202+
203+
diesel::insert_into(cache_tags_backfills::table)
204+
.values(row)
205+
.execute(&mut conn)
206+
.await
207+
.unwrap();
208+
209+
let names = load_crate_names(&opts(true, &[]), &conn).await.unwrap();
210+
assert_eq!(names, vec!["bar"]);
211+
}
212+
}

src/bin/crates-admin/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
extern crate tracing;
33

44
mod analyze_crates;
5+
mod backfill_cache_tags;
56
mod build_crate_zips;
67
mod default_versions;
78
mod delete_crate;
@@ -23,6 +24,7 @@ mod yank_version;
2324
#[command(name = "crates-admin")]
2425
enum Command {
2526
AnalyzeCrates(analyze_crates::Options),
27+
BackfillCacheTags(backfill_cache_tags::Options),
2628
BuildCrateZips(build_crate_zips::Options),
2729
RenderOgImages(render_og_images::Opts),
2830
DeleteCrate(delete_crate::Opts),
@@ -58,6 +60,7 @@ async fn main() -> anyhow::Result<()> {
5860

5961
match command {
6062
Command::AnalyzeCrates(opts) => analyze_crates::run(opts).await,
63+
Command::BackfillCacheTags(opts) => backfill_cache_tags::run(opts).await,
6164
Command::BuildCrateZips(opts) => build_crate_zips::run(opts).await,
6265
Command::RenderOgImages(opts) => render_og_images::run(opts).await,
6366
Command::DeleteCrate(opts) => delete_crate::run(opts).await,

0 commit comments

Comments
 (0)