You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The pkgsite worker computes the imported-by counts for all packages in a single computation, triggered by a Cloud Scheduler job. This has worked fine for years.
Problem
We've added counts for number of importing modules, and also there are many more packages than there used to be. So the request times out, exceeding the maximum Cloud Scheduler timeout of 30 minutes.
Solution
Compute the counts incrementally. Each Scheduler run will do as much as it can in the time budget.
It will be efficient to split the work up by package path. Both relevant tables, search_documents and imports_unique, are keyed by the package path, so we can efficiently scan any contiguous segment of the tables.
The main complication is storing progress. We really just need one value, the last package path reached previously. But we propose to store more information about each run: start and end time, number of packages processed, and error if any. This will give us insight into the runs so we can debug and optimize them. The new table, import_count_runs, won't take up significant space for a long time if ever, and we can always truncate it: at worst that will result in some duplicate work. (We could also periodically delete rows older than, say, a month.)
Alternative solutions
We could adjust the import counts as we insert and delete modules. This would spread the logic out in several places, and its correctness would not be obvious. Doing it all in one place is much clearer.
We could continue doing all the work at once, but with a detached context that doesn't get cancelled when the Cloud Scheduler job times out. This would be the smallest code change, but it has two problems:
The scheduler jobs would always appear to succeed, even if the actual computation failed.
The workers die periodically, so the work might not always finish, or might rarely finish. (For example, if the computation exceeds available memory, it would always fail and we wouldn't know if we didn't read the logs.)
It doesn't scale. As the size of the search_documents table grows, the failures would happen more frequently.
Thanks for looking into this. I think it does make sense to ensure that each run does as much computation within the given time budget.
I was wondering if you had considered an alternate approach that doesn't involve a new import_count_runs table and an alphabetical cursor.
Handling packages that drop to 0 importers:
If we scan imports_unique by package path, packages that lose all of their importers will never be encountered because rows are deleted from imports_unique when a module no longer imports them. As a result, their imported_by_count in search_documents would never be reset to 0.
Cursor state management:
Tracking last_package_path requires wrap-around handling (Z to A), crash recovery (if a run fails before persisting its cursor), handling overlaps/retries from Cloud Scheduler, and maintaining/cleaning up the import_count_runs table over time.
Latency for new packages:
A newly ingested package (e.g. at cloud.google.com/...) published while the cursor is at github.com/... would have to wait for the cursor to traverse the rest of the alphabet and wrap around before its initial count is computed.
An alternative would involve no new table and just utilize the existing idx_search_documents_imported_by_module_count_updated_at index on search_documents
In a loop bounded by the time budget we would:
Pick the $N$ oldest packages (e.g. $N = 1{,}000$):
SELECT package_path_id, package_path
FROM search_documents
ORDER BY imported_by_module_count_updated_at ASC NULLS FIRST
LIMIT $1
FOR UPDATE SKIP LOCKED;
Fetch active importers for just those N packages: From imports_unique we could just retrieve the active importers for those packages.
Compute counts and update: Update search_documents with the updated counts.
This is just an idea, but we get the following benefits:
It means that we would not need to perform any separate cursor management and maintain a separate table.
There is immediate processing of new packages. NULLS FIRST ensures that newly ingested packages are processed on the next batch.
One difference: I only look at packages that haven't been updated in at least a week. That avoids constant writing to search_documents. (Assuming we get through everything in <1 week, which I think is feasible—the query is very fast. If not we could do it less frequently.) Most of the numbers change very slowly, so I think the delay is fine. I'm mindful of the write amplification from the updates to symbol_search_documents, and the fact that all those changes need to be replicated. (I'm not sure we'll be able to remove the symbol_search_documents columns after all, despite https://go.dev/cl/836405.)
Activity
jba commentedon Sep 21, 2026
/cc @ethanalee-work @hyangah
ethanalee-work commentedon Sep 22, 2026
Thanks for looking into this. I think it does make sense to ensure that each run does as much computation within the given time budget.
I was wondering if you had considered an alternate approach that doesn't involve a new
import_count_runstable and an alphabetical cursor.If we scan
imports_uniqueby package path, packages that lose all of their importers will never be encountered because rows are deleted fromimports_uniquewhen a module no longer imports them. As a result, theirimported_by_countinsearch_documentswould never be reset to 0.Tracking
last_package_pathrequires wrap-around handling (Z to A), crash recovery (if a run fails before persisting its cursor), handling overlaps/retries from Cloud Scheduler, and maintaining/cleaning up theimport_count_runstable over time.A newly ingested package (e.g. at
cloud.google.com/...) published while the cursor is atgithub.com/...would have to wait for the cursor to traverse the rest of the alphabet and wrap around before its initial count is computed.An alternative would involve no new table and just utilize the existing
idx_search_documents_imported_by_module_count_updated_atindex onsearch_documentsIn a loop bounded by the time budget we would:
Pick the$N$ oldest packages (e.g. $N = 1{,}000$ ):
Fetch active importers for just those N packages: From imports_unique we could just retrieve the active importers for those packages.
Compute counts and update: Update
search_documentswith the updated counts.This is just an idea, but we get the following benefits:
NULLS FIRSTensures that newly ingested packages are processed on the next batch.jba commentedon Sep 25, 2026
I basically did that before reading this. https://go.dev/cl/838325
One difference: I only look at packages that haven't been updated in at least a week. That avoids constant writing to search_documents. (Assuming we get through everything in <1 week, which I think is feasible—the query is very fast. If not we could do it less frequently.) Most of the numbers change very slowly, so I think the delay is fine. I'm mindful of the write amplification from the updates to symbol_search_documents, and the fact that all those changes need to be replicated. (I'm not sure we'll be able to remove the symbol_search_documents columns after all, despite https://go.dev/cl/836405.)
Doing nulls first is a good idea.
gopherbot commentedon Sep 25, 2026
Change https://go.dev/cl/838325 mentions this issue:
internal/postgres: count module importsgopherbot commentedon Sep 29, 2026
Change https://go.dev/cl/841245 mentions this issue:
internal/postgres: update stale package importersgopherbot commentedon Sep 29, 2026
Change https://go.dev/cl/836405 mentions this issue:
internal/postgres: refine trigger for symbol searchgopherbot commentedon Sep 29, 2026
Change https://go.dev/cl/839545 mentions this issue:
internal/postgres: use query for package importersinternal/postgres: count module imports
internal/postgres: use query for package importers
internal/postgres: refine trigger for symbol search
gopherbot commentedon Oct 5, 2026
Change https://go.dev/cl/843545 mentions this issue:
internal/postgres: simplify test-only import count funcinternal/postgres: update stale package importers
internal/postgres: simplify test-only import count func