mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
Merge pull request #682 from peco/fix-filter-apply-error-handling
Report filter Apply errors to status bar
This commit is contained in:
commit
ced740f32d
55
filter.go
55
filter.go
|
|
@ -15,16 +15,17 @@ import (
|
||||||
"github.com/peco/peco/pipeline"
|
"github.com/peco/peco/pipeline"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFilterProcessor(f filter.Filter, q string, bufSize int) *filterProcessor {
|
func newFilterProcessor(f filter.Filter, q string, bufSize int, onError func(error)) *filterProcessor {
|
||||||
return &filterProcessor{
|
return &filterProcessor{
|
||||||
filter: f,
|
filter: f,
|
||||||
query: q,
|
query: q,
|
||||||
bufSize: bufSize,
|
bufSize: bufSize,
|
||||||
|
onError: onError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) {
|
func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) {
|
||||||
acceptAndFilter(ctx, fp.filter, fp.bufSize, in, out)
|
acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.onError, in, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// orderedChunk is a batch of lines tagged with a sequence number
|
// orderedChunk is a batch of lines tagged with a sequence number
|
||||||
|
|
@ -40,9 +41,18 @@ type orderedResult struct {
|
||||||
matched []line.Line
|
matched []line.Line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reportFilterError calls onError with a non-context-cancellation error.
|
||||||
|
// If the context is already cancelled or onError is nil, it does nothing.
|
||||||
|
func reportFilterError(ctx context.Context, err error, onError func(error)) {
|
||||||
|
if err == nil || ctx.Err() != nil || onError == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
|
|
||||||
// flusher is the single-threaded fallback used when the filter does not
|
// flusher is the single-threaded fallback used when the filter does not
|
||||||
// support parallel execution (e.g. Fuzzy with sortLongest).
|
// support parallel execution (e.g. Fuzzy with sortLongest).
|
||||||
func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput) {
|
func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput, onError func(error)) {
|
||||||
if pdebug.Enabled {
|
if pdebug.Enabled {
|
||||||
g := pdebug.Marker("flusher goroutine")
|
g := pdebug.Marker("flusher goroutine")
|
||||||
defer g.End()
|
defer g.End()
|
||||||
|
|
@ -62,7 +72,9 @@ func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, do
|
||||||
if pdebug.Enabled {
|
if pdebug.Enabled {
|
||||||
pdebug.Printf("flusher: %#v", buf)
|
pdebug.Printf("flusher: %#v", buf)
|
||||||
}
|
}
|
||||||
_ = f.Apply(ctx, buf, out)
|
if err := f.Apply(ctx, buf, out); err != nil {
|
||||||
|
reportFilterError(ctx, err, onError)
|
||||||
|
}
|
||||||
buffer.ReleaseLineListBuf(buf)
|
buffer.ReleaseLineListBuf(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +82,7 @@ func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, do
|
||||||
|
|
||||||
// parallelFlusher distributes filter work across multiple goroutines
|
// parallelFlusher distributes filter work across multiple goroutines
|
||||||
// and merges the results back in sequence order.
|
// and merges the results back in sequence order.
|
||||||
func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan orderedChunk, done chan struct{}, out pipeline.ChanOutput) {
|
func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan orderedChunk, done chan struct{}, out pipeline.ChanOutput, onError func(error)) {
|
||||||
if pdebug.Enabled {
|
if pdebug.Enabled {
|
||||||
g := pdebug.Marker("parallelFlusher goroutine")
|
g := pdebug.Marker("parallelFlusher goroutine")
|
||||||
defer g.End()
|
defer g.End()
|
||||||
|
|
@ -107,13 +119,19 @@ func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan ordered
|
||||||
var matched []line.Line
|
var matched []line.Line
|
||||||
if canCollect {
|
if canCollect {
|
||||||
// Fast path: collect results directly into a slice
|
// Fast path: collect results directly into a slice
|
||||||
matched, _ = collector.ApplyCollect(ctx, chunk.lines)
|
var err error
|
||||||
|
matched, err = collector.ApplyCollect(ctx, chunk.lines)
|
||||||
|
if err != nil {
|
||||||
|
reportFilterError(ctx, err, onError)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: use channel-based Apply for filters that
|
// Fallback: use channel-based Apply for filters that
|
||||||
// don't implement Collector (e.g. ExternalCmd)
|
// don't implement Collector (e.g. ExternalCmd)
|
||||||
collectCh := make(chan line.Line, len(chunk.lines))
|
collectCh := make(chan line.Line, len(chunk.lines))
|
||||||
go func(chunk orderedChunk) {
|
go func(chunk orderedChunk) {
|
||||||
_ = f.Apply(ctx, chunk.lines, pipeline.ChanOutput(collectCh))
|
if err := f.Apply(ctx, chunk.lines, pipeline.ChanOutput(collectCh)); err != nil {
|
||||||
|
reportFilterError(ctx, err, onError)
|
||||||
|
}
|
||||||
close(collectCh)
|
close(collectCh)
|
||||||
}(chunk)
|
}(chunk)
|
||||||
matched = make([]line.Line, 0, len(chunk.lines)/2)
|
matched = make([]line.Line, 0, len(chunk.lines)/2)
|
||||||
|
|
@ -197,10 +215,10 @@ func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan ordered
|
||||||
// It batches incoming lines and dispatches them to the filter, using parallel
|
// It batches incoming lines and dispatches them to the filter, using parallel
|
||||||
// workers when the filter supports it.
|
// workers when the filter supports it.
|
||||||
func AcceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in <-chan line.Line, out pipeline.ChanOutput) {
|
func AcceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in <-chan line.Line, out pipeline.ChanOutput) {
|
||||||
acceptAndFilter(ctx, f, configBufSize, in, out)
|
acceptAndFilter(ctx, f, configBufSize, nil, in, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in <-chan line.Line, out pipeline.ChanOutput) {
|
func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) {
|
||||||
useParallel := f.SupportsParallel() && runtime.GOMAXPROCS(0) > 1
|
useParallel := f.SupportsParallel() && runtime.GOMAXPROCS(0) > 1
|
||||||
|
|
||||||
buf := buffer.GetLineListBuf()
|
buf := buffer.GetLineListBuf()
|
||||||
|
|
@ -214,26 +232,26 @@ func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in
|
||||||
}
|
}
|
||||||
|
|
||||||
if useParallel {
|
if useParallel {
|
||||||
acceptAndFilterParallel(ctx, f, bufsiz, buf, in, out)
|
acceptAndFilterParallel(ctx, f, bufsiz, buf, onError, in, out)
|
||||||
} else {
|
} else {
|
||||||
acceptAndFilterSerial(ctx, f, bufsiz, buf, in, out)
|
acceptAndFilterSerial(ctx, f, bufsiz, buf, onError, in, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, in <-chan line.Line, out pipeline.ChanOutput) {
|
func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) {
|
||||||
flush := make(chan []line.Line)
|
flush := make(chan []line.Line)
|
||||||
flushDone := make(chan struct{})
|
flushDone := make(chan struct{})
|
||||||
go flusher(ctx, f, flush, flushDone, out)
|
go flusher(ctx, f, flush, flushDone, out, onError)
|
||||||
defer func() { <-flushDone }()
|
defer func() { <-flushDone }()
|
||||||
defer close(flush)
|
defer close(flush)
|
||||||
|
|
||||||
batchAndFlush(ctx, bufsiz, buf, in, flush, func(b []line.Line) []line.Line { return b })
|
batchAndFlush(ctx, bufsiz, buf, in, flush, func(b []line.Line) []line.Line { return b })
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptAndFilterParallel(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, in <-chan line.Line, out pipeline.ChanOutput) {
|
func acceptAndFilterParallel(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) {
|
||||||
flush := make(chan orderedChunk)
|
flush := make(chan orderedChunk)
|
||||||
flushDone := make(chan struct{})
|
flushDone := make(chan struct{})
|
||||||
go parallelFlusher(ctx, f, flush, flushDone, out)
|
go parallelFlusher(ctx, f, flush, flushDone, out, onError)
|
||||||
defer func() { <-flushDone }()
|
defer func() { <-flushDone }()
|
||||||
defer close(flush)
|
defer close(flush)
|
||||||
|
|
||||||
|
|
@ -401,7 +419,12 @@ func (f *Filter) Work(ctx context.Context, q *hub.Payload[string]) {
|
||||||
p.SetSource(src)
|
p.SetSource(src)
|
||||||
|
|
||||||
ctx = selectedFilter.NewContext(ctx, query)
|
ctx = selectedFilter.NewContext(ctx, query)
|
||||||
p.Add(newFilterProcessor(selectedFilter, query, state.config.FilterBufSize))
|
// Report non-cancellation filter errors (e.g. regex compilation failures)
|
||||||
|
// to the status bar so the user can see why results are missing.
|
||||||
|
onFilterError := func(err error) {
|
||||||
|
state.Hub().SendStatusMsg(ctx, err.Error(), 5*time.Second)
|
||||||
|
}
|
||||||
|
p.Add(newFilterProcessor(selectedFilter, query, state.config.FilterBufSize, onFilterError))
|
||||||
|
|
||||||
buf := NewMemoryBuffer(srcSize / 4)
|
buf := NewMemoryBuffer(srcSize / 4)
|
||||||
p.SetDestination(buf)
|
p.SetDestination(buf)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ package peco
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/peco/peco/filter"
|
"github.com/peco/peco/filter"
|
||||||
|
|
@ -264,3 +266,52 @@ func TestIncrementalFiltering(t *testing.T) {
|
||||||
require.Contains(t, displayStrings, "foobaz entry")
|
require.Contains(t, displayStrings, "foobaz entry")
|
||||||
require.Contains(t, displayStrings, "the foobird flies")
|
require.Contains(t, displayStrings, "the foobird flies")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errorFilter is a mock filter that always returns an error from Apply.
|
||||||
|
type errorFilter struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *errorFilter) Apply(_ context.Context, _ []line.Line, _ pipeline.ChanOutput) error {
|
||||||
|
return f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *errorFilter) BufSize() int { return 0 }
|
||||||
|
func (f *errorFilter) NewContext(ctx context.Context, _ string) context.Context { return ctx }
|
||||||
|
func (f *errorFilter) String() string { return "error-filter" }
|
||||||
|
func (f *errorFilter) SupportsParallel() bool { return false }
|
||||||
|
|
||||||
|
// TestFilterApplyErrorReporting verifies that when a filter's Apply method
|
||||||
|
// returns an error, the error is propagated to the onError callback (which in
|
||||||
|
// production sends a status bar message to the user).
|
||||||
|
func TestFilterApplyErrorReporting(t *testing.T) {
|
||||||
|
simulatedErr := errors.New("simulated filter error")
|
||||||
|
ef := &errorFilter{err: simulatedErr}
|
||||||
|
|
||||||
|
inputLines := []line.Line{
|
||||||
|
line.NewRaw(0, "alpha", false, false),
|
||||||
|
line.NewRaw(1, "bravo", false, false),
|
||||||
|
}
|
||||||
|
|
||||||
|
in := make(chan line.Line, len(inputLines))
|
||||||
|
for _, l := range inputLines {
|
||||||
|
in <- l
|
||||||
|
}
|
||||||
|
close(in)
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
var reported []error
|
||||||
|
onError := func(err error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
reported = append(reported, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(chan line.Line, len(inputLines))
|
||||||
|
acceptAndFilter(context.Background(), ef, 0, onError, in, pipeline.ChanOutput(out))
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
require.Len(t, reported, 1, "onError should have been called once")
|
||||||
|
require.Equal(t, simulatedErr, reported[0])
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -525,4 +525,5 @@ type filterProcessor struct {
|
||||||
filter filter.Filter
|
filter filter.Filter
|
||||||
query string
|
query string
|
||||||
bufSize int
|
bufSize int
|
||||||
|
onError func(error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ func TestIssue557_FilterBufSize(t *testing.T) {
|
||||||
// Use a configBufSize large enough to hold all lines in a single batch.
|
// Use a configBufSize large enough to hold all lines in a single batch.
|
||||||
// This ensures the fuzzy filter sees all lines at once and can sort globally.
|
// This ensures the fuzzy filter sees all lines at once and can sort globally.
|
||||||
configBufSize := totalLines + 100
|
configBufSize := totalLines + 100
|
||||||
fp := newFilterProcessor(f, query, configBufSize)
|
fp := newFilterProcessor(f, query, configBufSize, nil)
|
||||||
|
|
||||||
// Set up pipeline: source -> filterProcessor -> destination
|
// Set up pipeline: source -> filterProcessor -> destination
|
||||||
p := pipeline.New()
|
p := pipeline.New()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue