Repository navigation
Conversation
compact_lore_tables() skipped the lore table entirely and never touched lore_vectors, so neither ever dropped an old version. Every hourly refresh rewrites the five lore FTS indices in full (the body index alone is over 800 MB) and appends dozens of fragments, and all of it stayed on disk: on one host lore grew from 3.9 GB to 20 GB in two days, 17 GB of it superseded FTS indices. It also wedged FTS updates. optimize_lore_fts_indices() refuses to run once lore.lance/_indices holds more than 100 entries, and with nothing pruned the count only grew, by five per run. After about 14 hourly runs the indices stopped covering new mail for good. Prune old versions of lore from compact_lore_tables(), which only the lore run calls. Its Compact and index optimization phases, which the existing comment blames for running out of memory, stay skipped; prune reads only each manifest's index metadata. optimize_database() still skips lore: with delete_unverified set, a prune from another process could delete files a running lore refresh has not committed. Add lore_vectors to the tables compact_lore_tables() handles. Put the prune in one helper, prune_old_versions(), used by compact_lore_tables(), optimize_database() and create_lore_fts_indices(), which carried its own copy of the same call to clean up after rebuilding the FTS indices. The prune runs before the run's FTS update, so one superseded set of FTS indices remains on disk until the next run. It deletes their files but leaves their directories behind, so count only non-empty entries in the _indices guard.
Each lore parser thread sends a batch once it holds batch_size emails, and flushes whatever it has left when the commit list runs out. On an hourly refresh that adds a few hundred emails per archive, every thread ends up with a short batch, and each of them became its own append to lore and lore_indexed_commits: one host's refreshes added 40 to 80 fragments to each table per run, and the lore_vectors step then made one merge_insert per fragment. A parser only sends a short batch when its input has run out, so when an inserter receives one, keep receiving until it holds at least batch_size emails or every parser has finished. Full batches received on their own are inserted as before, and a merged batch stays under twice batch_size. In a simulated hourly refresh of 50 new emails with 32 parser threads, lore gains one fragment per run instead of 32.
update_lore_vectors() workers send one Vec per lore RecordBatch that still has emails without a vector, and the inserter ran a merge_insert into lore_vectors for each of them. On an incremental run most of those hold only a few vectors, while every merge_insert joins against the whole table: one host's day of new mail (6,228 emails) took about 1,600 merge_inserts and 56 minutes, nearly all of it in those joins, and each hourly run added 40 to 80 fragments to lore_vectors. Buffer vectors in the inserter and write once at least 8192 have accumulated, flushing the remainder when the workers finish. The merge semantics are unchanged. merge_insert inserts every unmatched source row, so keep one entry per message_id in a buffer, as separate merges used to. An hourly-sized run now adds one fragment. The progress bar still advances on every Vec the inserter receives, not at each write, so on an incremental run it does not sit at zero until the final write and then jump to the total. Its count can now run ahead of what has been written, so its message reads "Vectorized N emails" rather than "Inserted N vectors".
A cold lore build appends one lore_indexed_commits fragment per inserted batch: 1,013 for a production-sized set of archives, 3,660 for an upstream build. optimize_single_table() skips compaction above 500 fragments and nothing else brings the count down, so the table stays over the limit for good. Rebuilding with --clear, as the skip message advises, recreates the table over the limit. Exempt this table from the limit and keep the limit for every other table. Databases already over it compact on their next lore run that adds mail. The limit guards against a lance compaction CPU spin seen with thousands of small fragments (61e5493). lore_indexed_commits holds one 40-char SHA per row and no index. On lance 4.0.0 a table of that shape compacts in under 5 s at both 1,013 and 3,660 fragments, with no spin, whether compacted alone, as compact_lore_tables() does, or eight tables at a time, as optimize_database() does. The spin does not reproduce on lance 1.0.1 either, the version 61e5493 saw it on, so its cause is still unknown.
semcode-index printed "Lore table compaction completed successfully" and "FTS indices updated successfully" whatever had happened. The reasons for skipping or failing went to tracing::warn!, which the default SEMCODE_DEBUG level filters out, so the lore FTS indices stopped covering new mail for two days on one host without a line in the hourly unit's log. Print the FTS skip reasons and lore compaction problems to stderr. optimize_lore_fts_indices() and compact_lore_tables() now return whether they did their work, and the success lines are printed only when they did. optimize_single_table() also reports a table it did not compact because it has more than 500 fragments as a partial failure rather than success, since its fragment count only grows from there.
The lore BTree indices are created at schema setup, usually on an empty table, and nothing updates them afterwards: `optimize_single_table()` skips lore, and `optimize_lore_fts_indices()` merges only the FTS indices. In a database built from scratch they cover no rows, so every lookup by `message_id` scans the whole table, and so does every `in_reply_to` lookup a thread walk makes. A long-lived database had about 106,000 of its 998,000 emails outside them, a number every refresh adds to. `compact_lore_tables()` now rebuilds, after pruning old versions, the BTree indices on non-nullable columns: `message_id`, `git_commit_sha` and `date`, plus `from` and `subject` until the first FTS build replaces those with FTS indices. It builds them from the whole table each run rather than merging only the new rows into them, so no earlier failed run, and no rewrite of the table's fragments, can leave rows out. The BTrees on the nullable `in_reply_to` and `references` columns stay as they are, because lance 4.0.0 writes a BTree over a nullable string column of that size into pages it cannot decode: reading the index back panics, and so does any lookup that lands on a bad page. `in_reply_to` lookups therefore still scan the rows its index does not cover. A comment at the filter names the defect, so a lance upgrade can lift it. A failed rebuild is reported as a partial failure, like a failed prune. The rebuild runs as its own task, so a panic inside lance is reported the same way instead of ending semcode-index before the FTS update. `prune_lore_table()` is renamed `maintain_lore_table()` to match. On a copy of a production database (about 998,000 rows, load average 100 to 170 on 316 CPUs), rebuilding the three BTrees took 1.6 to 1.8 s every run, about twice as long as merging new rows, and peaked at 265 MB on its own. A run that rebuilds them peaks at 1.7 GB, most of it from listing the table's indices, which opens the FTS indices that the FTS update opens anyway. The index copies a rebuild replaces stay under `_indices/` until the next run prunes them, adding one BTree set to the non-empty entries `optimize_lore_fts_indices()` counts against its limit of 100.
Every lore run appends a few fragments to `lore`, and nothing merges them. A production database had reached 970 fragments, most of them a few rows, and queries that scan lore pay for each one: on a compacted copy, the thread lookup's `references` scan used 27 to 40 s of CPU instead of 51 to 56 s, and a date-bounded subject search 1.0 to 1.3 s instead of 2.0 to 2.8 s. `optimize_single_table()` skips the table, because a compaction that remaps every index on it peaked at 8.9 GB on a production-sized table, and panics on the `references` BTree, which lance 4.0.0 writes in a form it cannot read back. `compact_lore_tables()` now prunes, then compacts `lore` once it has more than 256 fragments, then rebuilds the BTrees as before. Fragments of 150,000 rows or more are not rewritten. Index remapping is deferred: lance records the old-to-new fragment mapping in its fragment-reuse index and leaves the indices as they are, which keeps memory low. The compaction runs as its own task, like the rebuild, so a panic in it is reported as a partial failure. Reads and the FTS update apply the fragment-reuse mapping, but lance 4.0.0's BTree update does not: after such a compaction it drops every row the index held in a rewritten fragment, while the index statistics still report full coverage. That is why the BTrees are rebuilt from the whole table each run rather than updated, and a new test checks that lookups find every row after a compaction of already-indexed fragments. facebookexperimental#122, which encodes lore vectors in 64-email chunks, should land before or with this commit: larger fragments let `update_lore_vectors()` hand its workers full 1,024-row batches, which raises the peak of a cold `--vectors` run. Costs and caveats: - On a copy of the production database (998,000 rows, host load 130 to 200 on 316 CPUs), the first compaction took 970 fragments to 6 in 4.0 s, and the BTree rebuild after it 1.6 s, with a process peak of 3.3 GB. - A table built from scratch by the facebookexperimental#112/facebookexperimental#113 lore pipeline holds only small fragments (997,703 rows in 1,043 fragments), so its first compaction rewrites all of it: 30 to 38 s and a 7.6 GB peak with the rewrite tasks run one at a time (8.4 GB at two, 9.0 GB at four), measured in a process of its own. About 3 GB of that is glibc arena fragmentation: with MALLOC_ARENA_MAX=2 it peaks at 4.2 GB. - A cold build now ends with this compaction, in the process whose indexing peaked at 14.5 GB; that whole run's peak was not measured. Neither was an upstream-built table, which holds more, smaller fragments. - Replaced fragments stay on disk until the next run's prune, so a query still reading them is not cut off. That is about 0.25 GB for an hour on production, and the whole table's data once on a cold database (2.3 GB more here). - lance 4.0.0 never trims the fragment-reuse index, so each compaction adds a version to it, a few KB after the first. At 256 fragments a table compacts about every 25 hourly runs, so about 14 versions a year.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #113: this branch is based on #113's head, so its first five commits are #113's. This PR adds the last two, each described below; review them one at a time.
lore: rebuild the non-nullable lore BTree indices each run
The lore BTree indices are created at schema setup, usually on an empty table, and nothing updates them afterwards:
optimize_single_table()skips lore, andoptimize_lore_fts_indices()merges only the FTS indices. In a database built from scratch they cover no rows, so every lookup bymessage_idscans the whole table, and so does everyin_reply_tolookup a thread walk makes. A long-lived database had about 106,000 of its 998,000 emails outside them, a number every refresh adds to.compact_lore_tables()now rebuilds, after pruning old versions, the BTree indices on non-nullable columns:message_id,git_commit_shaanddate, plusfromandsubjectuntil the first FTS build replaces those with FTS indices. It builds them from the whole table each run rather than merging only the new rows into them, so no earlier failed run, and no rewrite of the table's fragments, can leave rows out.The BTrees on the nullable
in_reply_toandreferencescolumns stay as they are, because lance 4.0.0 writes a BTree over a nullable string column of that size into pages it cannot decode: reading the index back panics, and so does any lookup that lands on a bad page.in_reply_tolookups therefore still scan the rows its index does not cover. A comment at the filter names the defect, so a lance upgrade can lift it.A failed rebuild is reported as a partial failure, like a failed prune. The rebuild runs as its own task, so a panic inside lance is reported the same way instead of ending semcode-index before the FTS update.
prune_lore_table()is renamedmaintain_lore_table()to match.On a copy of a production database (about 998,000 rows, load average 100 to 170 on 316 CPUs), rebuilding the three BTrees took 1.6 to 1.8 s every run, about twice as long as merging new rows, and peaked at 265 MB on its own. A run that rebuilds them peaks at 1.7 GB, most of it from listing the table's indices, which opens the FTS indices that the FTS update opens anyway. The index copies a rebuild replaces stay under
_indices/until the next run prunes them, adding one BTree set to the non-empty entriesoptimize_lore_fts_indices()counts against its limit of 100.lore: compact the lore table once it has many small fragments
Every lore run appends a few fragments to
lore, and nothing merges them. A production database had reached 970 fragments, most of them a few rows, and queries that scan lore pay for each one: on a compacted copy, the thread lookup'sreferencesscan used 27 to 40 s of CPU instead of 51 to 56 s, and a date-bounded subject search 1.0 to 1.3 s instead of 2.0 to 2.8 s.optimize_single_table()skips the table, because a compaction that remaps every index on it peaked at 8.9 GB on a production-sized table, and panics on thereferencesBTree, which lance 4.0.0 writes in a form it cannot read back.compact_lore_tables()now prunes, then compactsloreonce it has more than 256 fragments, then rebuilds the BTrees as before. Fragments of 150,000 rows or more are not rewritten. Index remapping is deferred: lance records the old-to-new fragment mapping in its fragment-reuse index and leaves the indices as they are, which keeps memory low. The compaction runs as its own task, like the rebuild, so a panic in it is reported as a partial failure.Reads and the FTS update apply the fragment-reuse mapping, but lance 4.0.0's BTree update does not: after such a compaction it drops every row the index held in a rewritten fragment, while the index statistics still report full coverage. That is why the BTrees are rebuilt from the whole table each run rather than updated, and a new test checks that lookups find every row after a compaction of already-indexed fragments.
#122, which encodes lore vectors in 64-email chunks, should land before or with this commit: larger fragments let
update_lore_vectors()hand its workers full 1,024-row batches, which raises the peak of a cold--vectorsrun.Costs and caveats: