Skip to content

Commit 86cc3d4

Browse files
committed
Don't lock more than 1 row at a time in update-downloads
Prior to this commit, `bin/update-downloads` will hold a lock on every row it updates for the entire batch of 1000. Each row gets locked when it is `updated`, but the lock isn't released until the whole transaction is comitted. This means that the first row in the batch is locked for the whole transaction, but the last row in the batch isn't locked longer than it should be. Processing 1000 rows is routinely taking more than 30 seconds to complete. Whenever it does, this causes requests to time out. Even when it doesn't, this locking behavior is the reason that our 99th percentile response time is getting to be so bad. The endpoint affected by this is `/crates/{name}/{version}/download`. In other words, this bug is causing `cargo install` to fail. Even when it doesn't, this bug is causing it to take seconds when it should take milliseconds. By establishing a transaction per row instead of per batch, we retain atomicity, without holding a lock for the whole batch. To keep everything atomic, we need to update the global counter per row instead of per batch. This relies on the invariant that no rows in `version_downloads` get touched once `date != CURRENT_DATE` other than by this script. I believe this is also the root cause of rust-lang#1304, and that we can maybe revert rust-lang#1312
1 parent c45d74b commit 86cc3d4

File tree

1 file changed

+49
-49
lines changed

1 file changed

+49
-49
lines changed

src/bin/update-downloads.rs

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,17 @@ fn main() {
3030

3131
fn update(conn: &PgConnection) -> QueryResult<()> {
3232
use version_downloads::dsl::*;
33+
3334
let mut max = Some(0);
3435
while let Some(m) = max {
35-
conn.transaction::<_, diesel::result::Error, _>(|| {
36-
let rows = version_downloads
37-
.filter(processed.eq(false))
38-
.filter(id.gt(m))
39-
.order(id)
40-
.limit(LIMIT)
41-
.load(conn)?;
42-
collect(conn, &rows)?;
43-
max = rows.last().map(|d| d.id);
44-
Ok(())
45-
})?;
36+
let rows = version_downloads
37+
.filter(processed.eq(false))
38+
.filter(id.gt(m))
39+
.order(id)
40+
.limit(LIMIT)
41+
.load(conn)?;
42+
collect(conn, &rows)?;
43+
max = rows.last().map(|d| d.id);
4644
}
4745
Ok(())
4846
}
@@ -61,50 +59,52 @@ fn collect(conn: &PgConnection, rows: &[VersionDownload]) -> QueryResult<()> {
6159
now.to_rfc2822()
6260
);
6361

64-
let mut total = 0;
6562
for download in rows {
6663
let amt = download.downloads - download.counted;
67-
total += i64::from(amt);
68-
69-
// Flag this row as having been processed if we're passed the cutoff,
70-
// and unconditionally increment the number of counted downloads.
71-
update(version_downloads::table.find(download.id))
72-
.set((
73-
version_downloads::processed.eq(download.date < cutoff),
74-
version_downloads::counted.eq(version_downloads::counted + amt),
75-
))
76-
.execute(conn)?;
77-
78-
// Update the total number of version downloads
79-
let crate_id = update(versions::table.find(download.version_id))
80-
.set(versions::downloads.eq(versions::downloads + amt))
81-
.returning(versions::crate_id)
82-
.get_result::<i32>(conn)?;
8364

84-
// Update the total number of crate downloads
85-
update(crates::table.find(crate_id))
86-
.set(crates::downloads.eq(crates::downloads + amt))
87-
.execute(conn)?;
65+
conn.transaction::<_, diesel::result::Error, _>(|| {
66+
// Flag this row as having been processed if we're passed the cutoff,
67+
// and unconditionally increment the number of counted downloads.
68+
update(version_downloads::table.find(download.id))
69+
.set((
70+
version_downloads::processed.eq(download.date < cutoff),
71+
version_downloads::counted.eq(version_downloads::counted + amt),
72+
))
73+
.execute(conn)?;
74+
75+
// Update the total number of version downloads
76+
let crate_id = update(versions::table.find(download.version_id))
77+
.set(versions::downloads.eq(versions::downloads + amt))
78+
.returning(versions::crate_id)
79+
.get_result::<i32>(conn)?;
80+
81+
// Update the total number of crate downloads
82+
update(crates::table.find(crate_id))
83+
.set(crates::downloads.eq(crates::downloads + amt))
84+
.execute(conn)?;
85+
86+
// Update the total number of crate downloads for today
87+
insert_into(crate_downloads::table)
88+
.values((
89+
crate_downloads::crate_id.eq(crate_id),
90+
crate_downloads::downloads.eq(amt),
91+
crate_downloads::date.eq(download.date),
92+
))
93+
.on_conflict(crate_downloads::table.primary_key())
94+
.do_update()
95+
.set(crate_downloads::downloads.eq(crate_downloads::downloads + amt))
96+
.execute(conn)?;
97+
98+
// Now that everything else for this crate is done, update the global counter of total
99+
// downloads
100+
update(metadata::table)
101+
.set(metadata::total_downloads.eq(metadata::total_downloads + (amt as i64)))
102+
.execute(conn)?;
88103

89-
// Update the total number of crate downloads for today
90-
insert_into(crate_downloads::table)
91-
.values((
92-
crate_downloads::crate_id.eq(crate_id),
93-
crate_downloads::downloads.eq(amt),
94-
crate_downloads::date.eq(download.date),
95-
))
96-
.on_conflict(crate_downloads::table.primary_key())
97-
.do_update()
98-
.set(crate_downloads::downloads.eq(crate_downloads::downloads + amt))
99-
.execute(conn)?;
104+
Ok(())
105+
})?;
100106
}
101107

102-
// After everything else is done, update the global counter of total
103-
// downloads.
104-
update(metadata::table)
105-
.set(metadata::total_downloads.eq(metadata::total_downloads + total))
106-
.execute(conn)?;
107-
108108
Ok(())
109109
}
110110

0 commit comments

Comments
 (0)