Skip to content

Commit e1dfaee

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 6f48eee commit e1dfaee

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

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

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)