|
155 | 155 | # "suppress nothing closed" errs toward hearing a finding twice, which is the |
156 | 156 | # cheaper mistake. |
157 | 157 | # |
| 158 | +# ONE SEARCH PER PATH DOES NOT SCALE, and a target writing its own command has |
| 159 | +# to be told so, because nothing about the failure is loud. The harness asks one |
| 160 | +# question carrying the whole scope and holds the answer to one bound, so a |
| 161 | +# command whose searches are proportional to the scope is killed partway through |
| 162 | +# and never answers — which the harness reads as dedupe not having run, on every |
| 163 | +# inspection, for as long as nobody looks. Measured against this tracker one |
| 164 | +# search costs roughly 0.85 seconds, so a 60-file scope spent close to a minute |
| 165 | +# against a 30-second bound. This branch therefore batches: BATCH path markers |
| 166 | +# are quoted and OR'd into one search, and the pages are unioned through the |
| 167 | +# composition below, which already deduplicates by URL. The cost of a search is |
| 168 | +# nearly flat in batch size — 5 markers measured 0.83 seconds, 10 measured 0.90 |
| 169 | +# and 20 measured about 1.0 — so a 60-file scope becomes three searches rather |
| 170 | +# than sixty. Neither the harness's bound nor the scope it hands over is what |
| 171 | +# changed: the scope is not capped, because a capped scope means inventing a |
| 172 | +# partial answer and an answer here means the whole question was answered. |
| 173 | +# |
| 174 | +# BATCHING IS ONLY SAFE WITH A FALLBACK, and the fallback is what a target |
| 175 | +# writing its own command must carry too. A search is capped at LIMIT results. |
| 176 | +# With one path per search that cap is per path; with many paths in one search a |
| 177 | +# filled page could be several paths' worth of issues truncated, and a truncated |
| 178 | +# page read as complete is a duplicate filed. So a batch whose page fills to the |
| 179 | +# limit, and a batch whose search fails, are both re-asked one path at a time — |
| 180 | +# which is also what keeps this safe on a tracker whose limit on query length is |
| 181 | +# tighter than this one's, since a batch that is too long to search falls back |
| 182 | +# rather than failing. A per-path search that fails after that still fails the |
| 183 | +# whole answer, on the rule above: reporting the paths that did answer would say |
| 184 | +# that nothing is filed against the ones that did not. |
| 185 | +# |
158 | 186 | # THE QUERY BRANCH IS THE SYNC BRANCH'S PAIR, and they are now the same file, |
159 | 187 | # which is what removes the way they used to be able to drift: the sync branch |
160 | 188 | # writes one searchable marker per path and records the whole payload under a |
@@ -325,10 +353,20 @@ IN_PROGRESS_OPTION="${L5_ITEM_IN_PROGRESS_OPTION:-In Progress}" |
325 | 353 | READY_TO_MERGE_OPTION="${L5_ITEM_READY_TO_MERGE_OPTION:-Ready to Merge}" |
326 | 354 |
|
327 | 355 | # --- what only the query job uses. Edit these. --------------------------- |
328 | | -# How many items one path's search may return. The harness bounds what it will |
| 356 | +# How many items one search may return. The harness bounds what it will |
329 | 357 | # read as well; this bound is about what the tracker is asked for. |
330 | 358 | LIMIT="${L5_QUERY_LIMIT:-50}" |
331 | 359 |
|
| 360 | +# How many path markers one search carries. It exists because the number of |
| 361 | +# searches, not the cost of one, is what stopped this branch answering a scope |
| 362 | +# of any size: see ONE SEARCH PER PATH DOES NOT SCALE above for the measurement |
| 363 | +# and for what the fallback below guarantees. It bounds two things at once — |
| 364 | +# how long one search's text is, for a tracker whose limit on query length is |
| 365 | +# tighter than this one's, and how many paths one filled page can hide, since a |
| 366 | +# batch whose page fills to LIMIT is re-asked path by path and a smaller batch |
| 367 | +# makes that fallback rarer. |
| 368 | +BATCH="${L5_QUERY_BATCH:-20}" |
| 369 | + |
332 | 370 | # --- the failure vocabularies ------------------------------------------- |
333 | 371 | # fail_transient is the sync branch's alone. Exit 75 means "the entry stays |
334 | 372 | # pending and a later sweep tries again", and nothing retries behind the query |
@@ -659,10 +697,74 @@ PATHS |
659 | 697 | # The query job |
660 | 698 | # ========================================================================== |
661 | 699 |
|
| 700 | +# The batched search's own state, held here rather than in do_query's locals |
| 701 | +# because query_batch reads and appends to all four. `found` accumulates the |
| 702 | +# pages every search returned, in the order they were made; the other three are |
| 703 | +# the batch being assembled. |
| 704 | +found="" |
| 705 | +batch_search="" |
| 706 | +batch_paths="" |
| 707 | +batch_count=0 |
| 708 | + |
| 709 | +# Search for one batch's markers at once, and fall back to one search per path |
| 710 | +# where the batch's answer cannot be trusted. |
| 711 | +# |
| 712 | +# Two answers cannot be trusted and both fall back rather than being read. A |
| 713 | +# search that exited non-zero says nothing about what is filed against any of |
| 714 | +# its paths. And a page holding LIMIT items is a page the tracker truncated: |
| 715 | +# with one path per search that cap is per path, but a batch's filled page could |
| 716 | +# be several paths' worth of issues cut off, and a truncated page read as |
| 717 | +# complete is a duplicate filed. The fallback is also what keeps this safe on a |
| 718 | +# tracker whose limit on query length is tighter than this one's — a batch too |
| 719 | +# long to search fails, and failing is what re-asks its paths one at a time. |
| 720 | +# |
| 721 | +# A per-path search that fails after that fails the whole answer, which is the |
| 722 | +# behaviour this branch has always had: reporting the paths that did answer |
| 723 | +# would say that nothing is filed against the ones that did not. |
| 724 | +query_batch() { |
| 725 | + local page returned fallback one marker |
| 726 | + |
| 727 | + echo "searching for ${batch_count} path marker(s) in one search" >&2 |
| 728 | + fallback=0 |
| 729 | + if page="$(gh issue list --search "$batch_search" --state all --limit "$LIMIT" \ |
| 730 | + --json number,title,body,url,state,stateReason 2>/dev/null)"; then |
| 731 | + returned="$(printf '%s' "$page" | jq 'length' 2>/dev/null)" || returned="" |
| 732 | + if [ -z "$returned" ]; then |
| 733 | + echo "the batched search's page could not be counted, so it is re-asked one path at a time" >&2 |
| 734 | + fallback=1 |
| 735 | + elif [ "$returned" -ge "$LIMIT" ]; then |
| 736 | + echo "the batched search filled its page of ${LIMIT}, so it may be truncated and is re-asked one path at a time" >&2 |
| 737 | + fallback=1 |
| 738 | + fi |
| 739 | + else |
| 740 | + echo "the batched search failed, so it is re-asked one path at a time" >&2 |
| 741 | + fallback=1 |
| 742 | + fi |
| 743 | + |
| 744 | + if [ "$fallback" -eq 0 ]; then |
| 745 | + found="${found}${page} |
| 746 | +" |
| 747 | + return 0 |
| 748 | + fi |
| 749 | + |
| 750 | + while IFS= read -r one; do |
| 751 | + [ -n "$one" ] || continue |
| 752 | + marker="${PATH_MARKER_PREFIX}${one}" |
| 753 | + echo "searching for ${marker}" >&2 |
| 754 | + page="$(gh issue list --search "\"${marker}\"" --state all --limit "$LIMIT" \ |
| 755 | + --json number,title,body,url,state,stateReason 2>/dev/null)" \ |
| 756 | + || fail "the search for ${one} failed, so what is filed is not known" |
| 757 | + found="${found}${page} |
| 758 | +" |
| 759 | + done <<BATCH_PATHS |
| 760 | +$batch_paths |
| 761 | +BATCH_PATHS |
| 762 | +} |
| 763 | + |
662 | 764 | do_query() { |
663 | 765 | require_tools fail |
664 | 766 |
|
665 | | - local question key body encoded asked paths found page one marker |
| 767 | + local question key body encoded asked paths one |
666 | 768 |
|
667 | 769 | question="$(cat)" || fail "the question could not be read from stdin" |
668 | 770 |
|
@@ -707,23 +809,39 @@ do_query() { |
707 | 809 | exit 0 |
708 | 810 | fi |
709 | 811 |
|
710 | | - # One search per path. A search that fails makes the whole answer unreliable — |
711 | | - # reporting the paths that did answer would say that nothing is filed against |
712 | | - # the ones that did not — so a failure here is a failure to answer. |
| 812 | + # BATCH markers to a search rather than one search per path, so the number of |
| 813 | + # searches is proportional to the scope divided by BATCH rather than to the |
| 814 | + # scope. Each batch's search text is its markers quoted and joined with OR; |
| 815 | + # every page goes into `found` and the composition below unions them. |
713 | 816 | found="" |
| 817 | + batch_search="" |
| 818 | + batch_paths="" |
| 819 | + batch_count=0 |
714 | 820 | while IFS= read -r one; do |
715 | 821 | [ -n "$one" ] || continue |
716 | | - marker="${PATH_MARKER_PREFIX}${one}" |
717 | | - echo "searching for ${marker}" >&2 |
718 | | - page="$(gh issue list --search "\"${marker}\"" --state all --limit "$LIMIT" \ |
719 | | - --json number,title,body,url,state,stateReason 2>/dev/null)" \ |
720 | | - || fail "the search for ${one} failed, so what is filed is not known" |
721 | | - found="${found}${page} |
| 822 | + if [ "$batch_count" -gt 0 ]; then |
| 823 | + batch_search="${batch_search} OR " |
| 824 | + fi |
| 825 | + batch_search="${batch_search}\"${PATH_MARKER_PREFIX}${one}\"" |
| 826 | + batch_paths="${batch_paths}${one} |
722 | 827 | " |
| 828 | + batch_count=$((batch_count + 1)) |
| 829 | + if [ "$batch_count" -ge "$BATCH" ]; then |
| 830 | + query_batch |
| 831 | + batch_search="" |
| 832 | + batch_paths="" |
| 833 | + batch_count=0 |
| 834 | + fi |
723 | 835 | done <<PATHS |
724 | 836 | $paths |
725 | 837 | PATHS |
726 | 838 |
|
| 839 | + # The last batch, which is short of BATCH whenever the scope does not divide |
| 840 | + # by it. A scope smaller than one batch is answered by exactly one search. |
| 841 | + if [ "$batch_count" -gt 0 ]; then |
| 842 | + query_batch |
| 843 | + fi |
| 844 | + |
727 | 845 | # One document on stdout and nothing else. Every item's fields are what the |
728 | 846 | # tracker said; nothing is invented for an item the searches did not return. |
729 | 847 | printf '%s' "$found" | jq -s -c \ |
|
0 commit comments