Company locations: the GeoNames index is built inside the first save of every worker, 6 s and 190 MB each time #399
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#399
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by readingjobs/places.pyand timingplaces._index()against the realgeonames-cities1000.txt(171,075 rows, 32 MB).Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atjobs/models.py:322-331(Company.save) andjobs/models.py:349(places.resolveinapply_location_guess), and atjobs/forms.py:352and:398(a location correction);jobs/places.py,fetch_geonames.py, the Dockerfile and the entrypoint are where they were.places._index()reads the whole cities table and folds every name, ascii name and alternate name into a dictionary in Python. It is built lazily, per process, on the firstplaces.resolve(), which is inside the first request that saves a company with a new location. Nothing builds it ahead of time, and the image runs three gunicorn workers that are each replaced after about 500 requests, so a person pays for the build again and again, and each process then keeps a large index for the rest of its life.jobs/places.py:71-104_index(@lru_cache(maxsize=1), per process),jobs/models.py:319-327Company.save→apply_location_guess→places.resolve(jobs/models.py:346), alsojobs/forms.py:351and:397(a location correction);docker/Dockerfile:354(--max-requests 500) and:362(--workers 3);docker/entrypoint.sh:26-30(the table fetched at first start). The comments size the table wrongly: "the table is a few megabytes" (jobs/places.py:77-78) and "The cities1000 table is twenty thousand rows" (jobs/management/commands/fetch_geonames.py:37-38)._index()takes 6.2 s on a desktop, produces 845,136 keys, holds 194 MB and peaks at 306 MB (tracemalloc); a laterresolve("Lisboa, Portugal")takes 3 ms. After a restart, or after a worker's 500th request, the next person who adds a company with a location, saves a capture as a listing, records an application for a new employer, corrects a location or imports a spreadsheet waits those seconds, several times longer on the small machines the project targets. The save runs insideATOMIC_REQUESTS, so on SQLite the whole instance waits with it. Each web worker that has resolved a place then carries about 190 MB more, three times over, plus the worker and scheduler processes if they save companies.fetch_geonameswrite an indexed lookup beside the text file (for example a small SQLite database keyed on the folded name) and haveresolve()query it. At the least, index the primary and ascii names plus alternates in the languages Postulo offers rather than every alternate, and build the index at worker start (gunicorn--preload, or on the scheduler's and web process's first tick) so no request pays for it. Correct the two comments.jobs/esco.pyloads the ESCO classification the same lazy way and would gain from the same change.Done when
tests/test_places.pysaves a company with a location while counting the rows read from the cities file, and fails today because the whole file is parsed on that save; or, with a lookup database, asserts thatresolvenever calls_index.tests/test_fetch_geonames.pychecks that the command writes the lookup it produces.Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#399).